【问题标题】:Computational complexity of Fibonacci Sequence斐波那契数列的计算复杂度
【发布时间】:2020-02-12 16:00:06
【问题描述】:

我了解 Big-O 表示法,但我不知道如何为许多函数计算它。特别是,我一直试图弄清楚斐波那契数列的原始版本的计算复杂性:

int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    else
        return Fibonacci(n - 1) + Fibonacci(n - 2);
}

斐波那契数列的计算复杂度是多少,如何计算?

【问题讨论】:

  • 在此处查看矩阵表格部分:en.wikipedia.org/wiki/Fibonacci_number。通过做这个矩阵 ^ n (以一种聪明的方式),你可以计算 O(lg n) 中的 Fib(n)。诀窍在于执行幂函数。关于这个确切的问题以及如何在 O(lg n) 中解决,iTunesU 上有一个非常好的讲座。该课程介绍了麻省理工学院第 3 课的算法(完全免费,如果您有兴趣,请查看)
  • 上述 cmets 都没有解决这个问题,这是关于朴素版本(在发布的代码中)的计算复杂性,而不是矩阵形式或非递归计算等更智能的版本。
  • 一个非常棒的视频here 讨论了递归实现的下限复杂度(2^n/2)和上限复杂度(2^n)。
  • 附注查询:斐波那契数列的朴素实现应该是迭代还是递归

标签: time-complexity big-o complexity-theory fibonacci


【解决方案1】:

您将计算 Fib(n) 的时间函数建模为计算 Fib(n-1) 的时间加上计算 Fib(n-2) 的时间加上将它们加在一起的时间 (O(1))。这是假设对同一 Fib(n) 的重复评估花费相同的时间 - 即不使用记忆。

T(n&lt;=1) = O(1)

T(n) = T(n-1) + T(n-2) + O(1)

你解决这个递归关系(例如使用生成函数),你最终会得到答案。

或者,您可以绘制递归树,其深度为n,并直观地找出该函数是渐近的O(2n)。然后你可以通过归纳来证明你的猜想。

基地:n = 1 很明显

假设T(n-1) = O(2n-1)因此

T(n) = T(n-1) + T(n-2) + O(1)等于

T(n) = O(2n-1) + O(2n-2) + O(1) = O(2n)

但是,正如评论中所指出的,这不是严格的限制。关于这个函数的一个有趣的事实是,T(n) 与Fib(n) 的值渐近相同,因为两者都定义为

f(n) = f(n-1) + f(n-2).

递归树的叶子总是返回 1。Fib(n) 的值是递归树中叶子返回的所有值的总和,等于叶子的计数。由于每个叶子需要 O(1) 来计算,T(n) 等于 Fib(n) x O(1)。因此,这个函数的严格界限是斐波那契数列本身 (~θ(1.6n))。您可以使用我上面提到的生成函数来找出这个紧密的界限。

【讨论】:

  • 把你的 StackOverflowException 当作一个笑话。指数时间很容易在 n 值很小的情况下被感知。
  • @MehrdadAfshari 你能解释一下为什么你选择 T(n-1) = O(2^n-1)。 T(n-1) 应该是 (n^2),因为斐波那契有调用 T(n-1)+T(n-2) 所以在对所有成本求和之后 (2*2*2....) 应该是2^n.
  • 归纳证明真的正确吗?似乎你同样可以假设 T(n) = O(n) 然后你会有 T(n) = O(n-1) + O(n-2) + O(1) = O(n)所以 T(n) = O(n) 但这显然不是真的?如果是正确的请解释..
  • @RichardFung 这里使用的逻辑不精确,归纳假设太弱了,因为里面已经包含了big-O。正确的假设是说对于某个固定的c,T(n)
  • "或者,您可以绘制递归树,其深度为 n,并且直观地计算出该函数渐近 O(2n)。" - 这是完全错误的。时间复杂度为 O(golden_ratio^n)。它永远不会接近 O(2^n)。如果您可以伸向无穷大,它将接近 O(golden_ratio^n)。这就是渐近线,两条线之间的距离必须接近 0。
【解决方案2】:

问问自己需要执行多少条语句,F(n) 才能完成。

对于F(1),答案是1(条件的第一部分)。

对于F(n),答案是F(n-1) + F(n-2)

那么什么函数满足这些规则呢?尝试an (a > 1):

an == a(n-1) + a(n-2)

除以a(n-2)

a2 == a + 1

求解a,得到(1+sqrt(5))/2 = 1.6180339887,也称为golden ratio

所以它需要指数级的时间。

【讨论】:

  • 30 票支持错误答案? :-) 是否遵循 1=F(1)=(1+sqrt(5))/2 ?那么另一个解决方案 (1-sqrt(5))/2 呢?
  • 不,1 不等于 1 + 1。问题中提到了满足这些规则的函数。
  • 答案没有错。无症状是对的。另一种解决方案是负面的,因此在物理上没有意义。
  • 有人能解释一下 a^n == a^(n-1) + a^(n-2) 是如何满足这些规则的吗?具体如何满足,请具体说明。
  • @frank 还记得任何树的时间复杂度都是 O(no_of_branches ^ depth_of_tree) 吗?对于任何一个分支 fib(N),鉴于它在底部不是一棵完美的树,最终高度为 N,但“平均分支数”略小于 2。所以,要得到这个确切的数字 @ 987654329@,我们假设对于任何分支 fib(n),O(n) 为x^n。因此x^n = x^n-1 + x^n-2.
【解决方案3】:

我同意 pgaur 和 rickerbh 的观点,recursive-fibonacci 的复杂度是 O(2^n)。

我通过一个相当简单的结论得出了同样的结论,但我相信推理仍然有效。

首先,这一切都是为了弄清楚在计算第 N 个斐波那契数时,递归斐波那契函数(从现在开始为 F())被调用了多少次。如果它在序列 0 到 n 中的每个数字被调用一次,那么我们有 O(n),如果它被每个数字调用 n 次,那么我们得到 O(n*n) 或 O(n^2),等等。

因此,当为数字 n 调用 F() 时,对于 0 到 n-1 之间的给定数字调用 F() 的次数会随着我们接近 0 而增加。

作为第一印象,在我看来,如果我们以视觉方式表示,每次为给定的数字调用 F() 绘制一个单位,湿得到一种金字塔形状(也就是说,如果我们水平居中单位)。像这样的:

n              *
n-1            **
n-2           ****  
...
2           ***********
1       ******************
0    ***************************

现在的问题是,随着 n 的增长,这个金字塔的底部扩大的速度有多快?

我们来看一个真实的案例,比如 F(6)

F(6)                 *  <-- only once
F(5)                 *  <-- only once too
F(4)                 ** 
F(3)                ****
F(2)              ********
F(1)          ****************           <-- 16
F(0)  ********************************    <-- 32

我们看到 F(0) 被调用了 32 次,即 2^5,在本示例中为 2^(n-1)。

现在,我们想知道 F(x) 被调用了多少次,我们可以看到 F(0) 被调用的次数只是其中的一部分。

如果我们将 F(6) 到 F(2) 行中的所有 * 移到 F(1) 行中,我们会看到 F(1) 和 F(0) 行现在长度相等。这意味着,当 n=6 时调用 F() 的总次数为 2x32=64=2^6。

现在,就复杂性而言:

O( F(6) ) = O(2^6)
O( F(n) ) = O(2^n)

【讨论】:

  • F(3) 只被调用 3 次而不是 4 次。第二个金字塔是错误的。
  • F(3) = 3, F(2) = 5, F(1) = 8, F(0) = 5。我会修复它,但我认为我无法挽救这个答案有一个编辑。
【解决方案4】:

关于specific problem over at MIT 的讨论非常好。在第 5 页,他们指出,如果您假设加法需要一个计算单位,则计算 Fib(N) 所需的时间与 Fib(N) 的结果密切相关。

因此,您可以直接跳到非常接近的斐波那契数列:

Fib(N) = (1/sqrt(5)) * 1.618^(N+1) (approximately)

因此,朴素算法的最坏情况性能是

O((1/sqrt(5)) * 1.618^(N+1)) = O(1.618^(N+1))

PS:如果您想了解更多信息,请在 Wikipedia 上讨论 closed form expression of the Nth Fibonacci number

【讨论】:

    【解决方案5】:

    你可以扩展它并进行可视化

         T(n) = T(n-1) + T(n-2) <
         T(n-1) + T(n-1) 
    
         = 2*T(n-1)   
         = 2*2*T(n-2)
         = 2*2*2*T(n-3)
         ....
         = 2^i*T(n-i)
         ...
         ==> O(2^n)
    

    【讨论】:

    • 我明白第一行。但是为什么最后有一个小于字符&lt;?你是怎么得到T(n-1) + T(n-1)的?
    • @QuaziIrfan :D 那是一个箭头。 -> [(不小于)。很抱歉对最后一行造成混淆]。对于第一行,嗯...T(n-1) &gt; T(n-2) 所以我可以更改T(n-2) 并输入T(n-1)。我只会得到一个对T(n-1) + T(n-2)仍然有效的上限
    【解决方案6】:

    递归算法的时间复杂度可以通过绘制递归树来更好的估计,此时绘制递归树的递归关系为T(n)=T(n-1)+T(n-2)+O(1 ) 请注意,每一步都需要 O(1) 表示恒定时间,因为它只进行一次比较以检查 if 块中 n 的值。递归树看起来像

              n
       (n-1)      (n-2)
    (n-2)(n-3) (n-3)(n-4) ...so on
    

    这里假设上面树的每一层都用 i 表示 因此,

    i
    0                        n
    1            (n-1)                 (n-2)
    2        (n-2)    (n-3)      (n-3)     (n-4)
    3   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)
    

    假设在 i 的特定值,树结束,这种情况将是当 n-i=1 时,因此 i=n-1,这意味着树的高度是 n-1。 现在让我们看看树中 n 层的每一层做了多少工作。请注意,每一步都需要 O(1) 时间,如递归关系中所述。

    2^0=1                        n
    2^1=2            (n-1)                 (n-2)
    2^2=4        (n-2)    (n-3)      (n-3)     (n-4)
    2^3=8   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)    ..so on
    2^i for ith level
    

    因为 i=n-1 是树的高度,在每个级别完成的工作将是

    i work
    1 2^1
    2 2^2
    3 2^3..so on
    

    因此,完成的总工作将是每个级别完成的工作的总和,因此它将是 2^0+2^1+2^2+2^3...+2^(n-1),因为 i=n -1。 按几何级数,这个和是 2^n,因此这里的总时间复杂度是 O(2^n)

    【讨论】:

      【解决方案7】:

      证明答案很好,但我总是需要手动进行几次迭代才能真正说服自己。于是我在白板上画了一个小的调用树,开始数节点。我将计数分为总节点、叶节点和内部节点。这是我得到的:

      IN | OUT | TOT | LEAF | INT
       1 |   1 |   1 |   1  |   0
       2 |   1 |   1 |   1  |   0
       3 |   2 |   3 |   2  |   1
       4 |   3 |   5 |   3  |   2
       5 |   5 |   9 |   5  |   4
       6 |   8 |  15 |   8  |   7
       7 |  13 |  25 |  13  |  12
       8 |  21 |  41 |  21  |  20
       9 |  34 |  67 |  34  |  33
      10 |  55 | 109 |  55  |  54
      

      立即跳出来的是叶节点的数量是fib(n)。需要更多迭代才能注意到内部节点的数量是fib(n) - 1。因此节点总数为2 * fib(n) - 1

      由于在对计算复杂度进行分类时去掉了系数,所以最终的答案是θ(fib(n))

      【讨论】:

      • (不,我没有在白板上绘制完整的 10 深度调用树。只有 5 深度。);)
      • 很好,我想知道递归 Fib 做了多少额外的添加。这不仅仅是将 1 添加到单个累加器 Fib(n) 次,有趣的是它仍然是 θ(Fib(n))
      • 请注意,一些(大多数)递归实现会花时间添加0,但:递归基本情况是01,因为它们确实是Fib(n-1) + Fib(n-2)。所以可能this link-only answer 中的3 * Fib(n) - 2 对于节点总数更准确,而不是2 * Fib(n) - 1
      • 我无法在叶节点上得到相同的结果。从 0 开始:F(0) -> 1 叶(本身); F(1) -> 1 片叶子(本身); F(2)-> 2 片叶子(F(1) 和 F(0)); F(3) -> 3 片叶子; F(5) -> 8 片叶子;等等
      【解决方案8】:

      它的下端以2^(n/2) 为界,上端以 2^n 为界(如其他 cmets 中所述)。该递归实现的一个有趣事实是它具有 Fib(n) 本身的严格渐近界。这些事实可以概括为:

      T(n) = Ω(2^(n/2))  (lower bound)
      T(n) = O(2^n)   (upper bound)
      T(n) = Θ(Fib(n)) (tight bound)
      

      如果您愿意,可以使用其closed form 进一步减少紧密绑定。

      【讨论】:

        【解决方案9】:

        通过绘制函数调用图表很容易计算。只需为每个 n 值添加函数调用,然后查看数字如何增长。

        大 O 是 O(Z^n),其中 Z 是黄金比例或大约 1.62。

        随着 n 的增加,莱昂纳多数和斐波那契数都接近这个比率。

        与其他大 O 问题不同,输入没有可变性,算法和算法的实现都明确定义。

        不需要一堆复杂的数学。简单地画出下面的函数调用并将函数与数字相匹配。

        或者,如果您熟悉黄金比例,您就会认出它。

        这个答案比公认的答案更正确,后者声称它将接近 f(n) = 2^n。它永远不会。它将接近 f(n) = golden_ratio^n。

        2 (2 -> 1, 0)
        
        4 (3 -> 2, 1) (2 -> 1, 0)
        
        8 (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                    (2 -> 1, 0)
        
        
        14 (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                    (2 -> 1, 0)
        
                    (3 -> 2, 1) (2 -> 1, 0)
        
        22 (6 -> 5, 4)
                    (5 -> 4, 3) (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                                (2 -> 1, 0)
        
                                (3 -> 2, 1) (2 -> 1, 0)
        
                    (4 -> 3, 2) (3 -> 2, 1) (2 -> 1, 0)
                                (2 -> 1, 0)
        

        【讨论】:

        • 你能提供任何关于黄金比例的说法的来源吗?
        【解决方案10】:

        好吧,据我说是O(2^n),因为在这个函数中,只有递归需要相当长的时间(分而治之)。我们看到,上面的函数将在树中继续,直到叶子接近我们到达F(n-(n-1))F(1) 的水平。所以,这里当我们记下树的每个深度遇到的时间复杂度时,求和序列为:

        1+2+4+.......(n-1)
        = 1((2^n)-1)/(2-1)
        =2^n -1
        

        这是2^n [ O(2^n) ]的顺序。

        【讨论】:

          【解决方案11】:

          由于计算中的重复,斐波那契的朴素递归版本在设计上是指数的:

          你正在计算的根:

          F(n) 取决于 F(n-1) 和 F(n-2)

          F(n-1) 再次依赖于 F(n-2) 和 F(n-3)

          F(n-2) 再次依赖于 F(n-3) 和 F(n-4)

          那么您在每个级别 2 递归调用在计算中浪费了大量数据,时间函数将如下所示:

          T(n) = T(n-1) + T(n-2) + C,C 为常数

          T(n-1) = T(n-2) + T(n-3) > T(n-2) 然后

          T(n) > 2*T(n-2)

          ...

          T(n) > 2^(n/2) * T(1) = O(2^(n/2))

          这只是一个下限,就您的分析而言应该足够了,但实时函数是相同斐波那契公式的常数因子,并且已知 closed form 是黄金比例的指数。

          此外,您可以像这样使用动态编程找到优化的斐波那契版本:

          static int fib(int n)
          {
              /* memory */
              int f[] = new int[n+1];
              int i;
          
              /* Init */
              f[0] = 0;
              f[1] = 1;
          
              /* Fill */
              for (i = 2; i <= n; i++)
              {
                  f[i] = f[i-1] + f[i-2];
              }
          
              return f[n];
          }
          

          这是经过优化的,只执行 n 步,但也是指数级的。

          成本函数定义为从输入大小到解决问题的步骤数。当您看到动态版本的斐波那契(n 步来计算表格)或最简单的算法来知道一个数字是否是素数时(sqrt(n) 来分析有效数的除数)。你可能认为这些算法是 O(n)O(sqrt(n)) 但这根本不是真的,原因如下: 算法的输入是一个数字:n,使用二进制表示,整数 n 的输入大小是 log2(n) 然后执行可变的变化

          m = log2(n) // your real input size
          

          找出步数作为输入大小的函数

          m = log2(n)
          2^m = 2^log2(n) = n
          

          那么作为输入大小函数的算法成本是:

          T(m) = n steps = 2^m steps
          

          这就是成本呈指数增长的原因。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-07
            相关资源
            最近更新 更多