【问题标题】:How to explain cached fibonacci algorithm complexity如何解释缓存的斐波那契算法复杂度
【发布时间】:2016-09-12 08:26:48
【问题描述】:

我试图解释正确缓存的斐波那契算法复杂性。这是代码(https://jsfiddle.net/msthhbgy/2/):

function allFib(n) {
  var memo = [];
  for (var i = 0; i < n; i++) {
    console.log(i + ":" + fib(i, memo))
  }
}

function fib(n, memo) {
  if (n < 0) return 0;
  else if (n === 1) return 1;
  else if (memo[n]) return memo[n];
  memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
  return memo[n];
}

allFib(5);

解决方案取自“Cracking the coding interview”并改编为javascript。 所以这是一个“不是很好”的函数调用树

我是这样想的:“最左边的分支(粗体)是进行评估的地方”,这绝对是第一次传递给 allFib 函数的数字。所以复杂度是O(n)。右侧的所有内容都将从缓存中获取,并且不需要额外的函数调用“。正确吗?以及如何将其连接到树“理论”。在这种情况下,树的深度和高度为 4,但是不是 5(接近 n 但不是它)。我希望答案不直观但更可靠。

【问题讨论】:

  • 当涉及缓存时,f(k) 被评估多少次(对于任何 k)?
  • memo[n] 未使用!
  • 是的,memo 未使用首先检查该值是否存在于 memo 中,如果不存在则只计算 fib() 并分配给 memo。 if(memo[n]){return memo[n]} else {memo[n] = fib(n - 1, memo) + fib(n - 2, memo);返回备忘录[n];}
  • 好吧,O(n) 是计算未缓存/新值的复杂性。访问缓存的复杂度为O(1),因此该算法的最终复杂度必须介于O(1)O(n) 之间,具体取决于n 和缓存的状态。
  • 是的,可以。此时您的“问题”是缓存提供给功能,因此您永远不知道该缓存中有多少值;在allFib 的情况下,您总是从一个新的(空)缓存开始。但是您可以重用缓存,然后对 allFib() 的第二次调用将从缓存中“计算”出相同的 5 个值。因此,总共只计算了 5 次计算的 10 个值,其余的来自缓存。通常我会将缓存烘焙到 fib 函数中并避免这种怀疑。

标签: javascript algorithm memoization


【解决方案1】:

这是一个真正使用缓存的函数:

function Fibonacci() {
  var memo = [0, 1];

  this.callCount = 0;

  this.calc = function(n) {
    this.callCount++;
    return n <= 0 ? 0 
         : memo[n] || (memo[n] = this.calc(n - 1) + this.calc(n - 2));
  }
}

var fib = new Fibonacci();

console.log('15! = ', fib.calc(15));
console.log('calls made: ', fib.callCount);
fib.callCount = 0; // reset counter
console.log('5! = ', fib.calc(5));
console.log('calls made: ', fib.callCount);
fib.callCount = 0;
console.log('18! = ', fib.calc(18));
console.log('calls made: ', fib.callCount);

函数调用次数为:

(n - min(i,n))*2+1

imemo 中的最后一个条目。

您可以通过 n = 18i = 15 的示例看到如下:

调用按以下顺序进行:

calc(18)
calc(17)   // this.calc(n-1) with n=18
calc(16)   // this.calc(n-1) with n=17
calc(15)   // this.calc(n-1) with n=16, this can be returned from memo
calc(14)   // this.calc(n-2) with n=16, this can be returned from memo
calc(15)   // this.calc(n-2) with n=17, this can be returned from memo
calc(16)   // this.calc(n-2) with n=18, this can be returned from memo

一般模式是 this.calc(n-1)this.calc(n-2) 被调用多次(当然),此外还有原始调用 calc(n)

这是您第一次以fib.calc(5) 调用fib.calc 时的动画。箭头显示进行的调用。越向左,递归越深。当相应的结果存储在 memo 中时,气泡会被着色:

i 是给定常数时,这显然是 O(n)

【讨论】:

    【解决方案2】:

    首先,检查是否为负 n,并将值移至零。

    然后检查一个值是否被缓存,取值。如果不是,则将值分配给缓存并返回结果。

    对于n === 0n === 1的特殊情况分配n

    function fibonacci(number) {
        function f(n) {
            return n in cache ?
                cache[n] :
                cache[n] = n === 0 || n === 1 ? n : f(n - 1) + f(n - 2);
        }
    
        var cache = [];
        return f(number);
    }
    
    console.log(fibonacci(15));
    console.log(fibonacci(5));

    按照Thomas 的建议,在cache 中使用预定义值。

    function fibonacci(number) {
        function f(n) {
            return n in cache ?
                cache[n] :
                cache[n] = f(n - 1) + f(n - 2);
        }
    
        var cache = [0, 1];
        return f(number);
    }
    
    console.log(fibonacci(15));
    console.log(fibonacci(5));

    【讨论】:

    • 我会通过预先计算 cache = [0,1] 来处理特殊情况,然后您可以从函数 n === 0 || n === 1 ? n : 中删除该部分。
    猜你喜欢
    • 2023-03-07
    • 1970-01-01
    • 2020-02-12
    • 1970-01-01
    • 2015-01-06
    • 2011-11-28
    相关资源
    最近更新 更多