关于大o:Fibonacci序列的计算复杂性

Computational complexity of Fibonacci Sequence

我了解big-o表示法,但我不知道如何计算许多函数。特别是,我一直在试图找出Fibonacci序列的原始版本的计算复杂性:

1
2
3
4
5
6
7
int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    else
        return Fibonacci(n - 1) + Fibonacci(n - 2);
}

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


您将计算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))。通过使用上面提到的生成函数,您可以发现这个紧边界。


只需问问自己需要执行多少语句来完成F(n)

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

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

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

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

除以a(n-2):

A2==A+ 1

求解a,得到(1+sqrt(5))/2 = 1.6180339887,也就是黄金分割率。

所以这需要指数时间。


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

我也得出了同样的结论,虽然相当简单,但我相信仍然是正确的推理。

首先,要计算在计算第n个斐波那契数时,递归斐波那契函数(f())被调用的次数。如果它在序列0到n中每个数字被调用一次,那么我们有O(n),如果它在每个数字中被调用N次,那么我们得到O(n*n),或者O(n^2),依此类推。

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

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

1
2
3
4
5
6
7
n              *
n-1            **
n-2           ****  
...
2           ***********
1       ******************
0    ***************************

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

让我们以一个真实的案例为例,例如F(6)

1
2
3
4
5
6
7
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。

现在,就复杂性而言:

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


在麻省理工学院有一个关于这个具体问题的非常好的讨论。在第5页,他们指出,如果假设一个加法需要一个计算单元,那么计算fib(n)所需的时间与fib(n)的结果密切相关。

因此,可以直接跳到斐波那契级数的非常接近的近似值:

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

因此,可以说,幼稚算法的最坏情况是

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

附言:如果你想了解更多信息,维基百科上有一个关于第n个斐波那契数的闭式表达式的讨论。


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

1
2
3
4
5
6
7
8
9
10
     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
2
3
4
5
6
7
8
9
10
11
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))


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

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

如果您愿意,可以使用它的封闭形式进一步减少紧束缚。


通过绘制递归树可以更好地估计递归算法的时间复杂度,在这种情况下,绘制递归树的递归关系为t(n)=t(n-1)+t(n-2)+o(1)。注意,每个步骤都采用O(1)表示常量时间,因为它只对if块中n的值进行一次比较。

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

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

1
2
3
4
5
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)时间。

1
2
3
4
5
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是树的高度,在每一级完成的工作将

1
2
3
4
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)


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

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

这是江户十一〔42〕的命令。


网址:http://www.ics.uci.edu/~eppstein/161/960109.html

时间(n)=3f(n)-2


Fibonacci的原始递归版本在设计上是指数级的,这是由于计算中的重复:

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

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

这只是一个下限,为了便于分析,应该足够了,但实时函数是一个常数的因子,由相同的斐波那契公式和封闭形式是已知的指数黄金比率。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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个步骤,但也是指数级的。

成本函数的定义从输入大小到解决问题的步骤数。当您看到fibonacci的动态版本(n个步骤计算表)或最容易知道数字是否是素数的算法(sqrt(n)来分析数字的有效除数)时。您可能认为这些算法是O(n)或O(sqrt(n)),但这并非如此,原因如下:您的算法的输入是一个数字:n,使用二进制表示法,整数n的输入大小是log2(n),然后对

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

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

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

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

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

这就是成本指数化的原因。


这样做效果更好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int Fib(unsigned int n)
{
    // first Fibonaci number is Fib(0)
    // second one is Fib(1) and so on

    // unsigned int m;  // m + current_n = original_n
    unsigned int a = 1; // Fib(m)
    unsigned int b = 0; // Fib(m-1)
    unsigned int c = 0; // Fib(m-2)

    while (n--)
    {
        c = b;
        b = a;
        a = b+c;
    }

    return a;
}