斐波那契数列的计算复杂度


330

我了解Big-O表示法,但是我不知道如何为许多函数计算它。特别是,我一直在尝试找出Fibonacci序列的朴素版本的计算复杂性:

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

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



3
请参阅此处的矩阵表单部分:en.wikipedia.org/wiki/Fibonacci_number。通过做这个矩阵^ n(一种巧妙的方法),您可以计算出O(lg n)中的Fib(n)。诀窍在于执行幂函数。关于这个确切的问题以及如何在O(lg n)中解决问题,iTunesU上有一个很好的演讲。本课程是介绍从MIT讲座3算法(它absolutley免费的,所以检查出来,如果你有兴趣)
阿里

1
上面的评论都没有解决这个问题,该问题与朴素版本的计算复杂度(在已发布的代码中)有关,而与诸如矩阵形式或非递归计算之类的更智能版本无关。
乔什·米尔索普

这里有一个非常不错的视频它讨论了递归实现的下限复杂度(2 ^ n / 2)和上限复杂度(2 ^ n)。
RBT

1
旁注查询:斐波那契数列的天真的实现应该是迭代的还是递归的
RBT

Answers:


374

您可以对时间函数进行建模,以Fib(n)作为要计算Fib(n-1)的时间总和加上要计算Fib(n-2)的时间再加上将它们加在一起的时间(O(1))。这是假设对同一对象的重复评估Fib(n)需要花费相同的时间-即不使用记忆。

T(n<=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)


29
还通过归纳证明。真好 +1
Andrew Rollings

虽然界限不紧。
Segfault上尉,08年

@Segfault上尉:是的。我澄清了答案。就像我在上面写的那样,使用GF方法将获得严格的限制。
Mehrdad Afshari

把您的StackOverflowException当作一个玩笑。指数时间很容易感知,n的值很小。
大卫·罗德里格斯(DavidRodríguez)-德里贝斯

1
“或者,您可以绘制递归树,该树的深度为n,并且直观地知道此函数渐近为O(2n)。” -这是完全错误的。时间复杂度为O(golden_ratio ^ n)。它永远不会接近O(2 ^ n)。如果您可以朝无穷远延伸,它将接近O(golden_ratio ^ n)。这就是渐近线,两条线之间的距离必须接近
0。– bob

133

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

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

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

那么,什么功能满足这些规则?尝试n(a> 1):

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

除以(n-2)

a 2 == a + 1

解决就a得到了(1+sqrt(5))/2 = 1.6180339887,否则称为黄金分割

因此,它花费了指数时间。


8
通过归纳证明。真好 +1
Andrew Rollings

2
30个投票答案错误?:-)遵循1 = F(1)=(1 + sqrt(5))/ 2吗?那么其他解决方案(1-sqrt(5))/ 2呢?
卡斯顿S

1
不,1不等于1 +1。问题中提到满足这些规则的功能。
molbdnilo 2014年

6
答案没有错。这是无症状的。另一个解决方案是负面的,因此从物理上讲没有意义。
大登

10
有人可以解释a ^ n == a ^(n-1)+ a ^(n-2)如何满足这些规则吗?具体如何满足,请具体说明。
2016年

33

我同意pgaur和rickerbh的观点,递归斐波那契的复杂度为O(2 ^ n)。

我得出的结论很简单,但我认为推理仍然有效。

首先,所有要弄清楚在计算第N个斐波那契数时递归斐波那契函数(从现在开始F())被调用多少次。如果它按从0到n的顺序被每个数字调用一次,则为O(n),如果被每个数字调用n次,则得到O(n * n)或O(n ^ 2),等等。

因此,当F()被调用为数字n时,在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为2x32 = 64 = 2 ^ 6时,调用总时间F()。

现在,就复杂性而言:

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

3
F(3)仅被调用3次,而不是4次。第二座金字塔是错误的。
阿维克

2
F(3)= 3,F(2)= 5,F(1)= 8,F(0)=5。我会修复它,但我认为我无法通过编辑挽救这个答案。
Bernhard Barker

31

麻省理工学院对这个特定问题进行了很好的讨论。在第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上讨论第N个斐波那契数闭式表达式


感谢您的课程链接。很不错的观察过
SwimBikeRun

16

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

     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)

1
我了解第一行。但是为什么<末尾会有一个少于字符呢?你是怎么得到的T(n-1) + T(n-1)
Quazi Irfan

@QuaziIrfan:D是箭头。-> [(不少于)。对最后一行感到困惑,我们深表歉意。对于第一行,嗯... T(n-1) > T(n-2)所以我可以更改T(n-2)并放置T(n-1)。我只会得到一个更高的界限,该界限仍然适用于T(n-1) + T(n-2)
Tony Tannous

10

它在下端2^(n/2)由2 ^ n 限制,在上端由2 ^ n限制(如其他注释中所述)。该递归实现的一个有趣的事实是,它具有Fib(n)本身的紧密渐近边界。这些事实可以概括为:

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

如果您愿意,可以使用其封闭形式进一步减小紧密边界。


10

证明答案是好的,但是我总是必须手动做几次迭代才能真正说服自己。因此,我在白板上绘制了一个小的调用树,并开始计算节点数。我将计数分为总节点,叶节点和内部节点。这是我得到的:

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层深。);)
benkc

很好,我想知道递归Fib有多少额外的功能。这不仅增加1了一个累加器的Fib(n)时间,而且有趣的是它仍然是精确的θ(Fib(n))
彼得·科德斯

请注意,尽管有一些(大多数)递归实现花费时间添加0:递归基本案例是0and 1,因为它们确实是Fib(n-1) + Fib(n-2)。所以,可能是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片叶子; 等
alexlomba87

9

可以通过绘制递归树来更好地估计递归算法的时间复杂度,在这种情况下,绘制递归树的递归关系为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的特定值下,树结束了,这种情况将在ni = 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

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


2

好吧,对我而言,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) ]


1

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

从根本上讲,您正在计算:

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个递归调用,这些调用浪费了计算中的大量数据,那么time函数将如下所示:

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))

这只是一个下限,对于您的分析目的来说就足够了,但是实时函数是相同的斐波那契公式的一个常数因子,并且已知闭合形式是黄金比率的指数。

此外,您可以使用动态编程来找到Fibonacci的优化版本,如下所示:

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,使用二进制表示法将输入大小指定为整数nlog2(n)然后进行变量更改

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

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

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

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

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

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


1

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

大O为O(Z ^ n),其中Z为黄金分割率或约1.62。

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

与其他Big 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)

1
您可以提供有关黄金分割率的任何信息吗?
Nico Haase

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.