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); } |
斐波那契数列的计算复杂度是多少?如何计算?
您将计算
您解决这个循环关系(例如,使用生成函数),最终得到答案。
或者,您可以绘制递归树,其深度为
基础:
假设
然而,正如评论中所指出的,这并不是严格限制。关于这个函数的一个有趣的事实是,t(n)与
递归树的叶总是返回1。
只需问问自己需要执行多少语句来完成
对于
对于
那么,什么函数满足这些规则呢?尝试A(A>1):
a n==a(n-1)+a(n-2)
除以a(n-2):
A2==A+ 1
求解
所以这需要指数时间。
我同意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 |
立即跳出来的是叶节点的数量是
由于在对计算复杂性进行分类时降低了系数,最终的答案是
其下端以
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)
根据我的说法,它是
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; } |