我理解大O符号,但不知道如何计算许多函数的复杂度。特别是,我一直在尝试弄清楚斐波那契数列的朴素版本的计算复杂度:
int Fibonacci(int n)
{
if (n <= 1)
return n;
else
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
斐波那契数列的计算复杂度是多少,如何计算?
我理解大O符号,但不知道如何计算许多函数的复杂度。特别是,我一直在尝试弄清楚斐波那契数列的朴素版本的计算复杂度:
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(2
n
)
。然后您可以通过归纳证明此猜想的正确性。n = 1
很显然T(n-1) = O(2
n-1
)
,因此
T(n) = T(n-1) + T(n-2) + O(1)
,它等价于
T(n) = O(2
n-1
) + O(2
n-2
) + O(1) = O(2
n
)
然而,正如评论中所指出的那样,这不是紧密边界。关于这个函数的一个有趣事实是 T(n)
渐近地与 Fib(n)
的值相同,因为两者都被定义为
f(n) = f(n-1) + f(n-2)
。Fib(n)
的值是递归树中所有叶子返回值的总和,即等于叶子的数量。由于每个叶子要花费 O(1) 来计算,因此 T(n)
等于 Fib(n) x O(1)
。因此,该函数的紧密边界是斐波那契数列本身(~θ(1.6
n
)
)。您可以使用我上面提到的生成函数来找到这个紧密边界。只需问自己需要执行多少语句才能完成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
,也被称为 黄金比率 。
所以它需要指数时间。
x
,我们假设对于任何分支fib(n),O(n)为 x^n
。因此 x^n = x^n-1 + x^n-2
。 - Lucia我同意pgaur和rickerbh的观点,递归斐波那契的时间复杂度为O(2^n)。
我通过一种相当简单但我认为仍然有效的推理得出了相同的结论。
首先,关键是要弄清楚在计算第N个斐波那契数时,递归斐波那契函数(从现在开始记为F())被调用的次数。如果它每个数字都被调用一次,我们就有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 时,F() 被调用的总次数为 2x32=64=2^6。
现在,就复杂度而言:
O( F(6) ) = O(2^6)
O( F(n) ) = O(2^n)
在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: 如果您想了解更多信息,可以在维基百科上查看 第N个斐波那契数的闭合形式表达式 的讨论。
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)
<
?你是怎么得到T(n-1) + T(n-1)
的? - Quazi IrfanT(n-1)>T(n-2)
,所以我可以改变T(n-2)
并放置T(n-1)
。我只会得到一个仍然适用于T(n-1)+T(n-2)
的更高上界。 - Tony通过绘制递归树,可以更好地估计递归算法的时间复杂度。在这种情况下,绘制递归树的递推关系为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)
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
证明的答案很好,但我总是要手动进行几次迭代才能真正让自己信服。所以我在白板上画了一个小的调用树,并开始数节点。我把计数分成总节点数、叶子节点数和内部节点数。这是我的结果:
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))
。
1
加到单个累加器中Fib(n)
次,而且有趣的是它仍然完全符合θ(Fib(n))
。 - Peter Cordes这个实现在下界上受到2^(n/2)
的限制,在上界上受到2^n的限制(正如其他评论中所提到的)。而该递归实现的一个有趣事实是它具有Fib(n)本身的紧密渐进界限。这些事实可以总结如下:
T(n) = Ω(2^(n/2)) (lower bound)
T(n) = O(2^n) (upper bound)
T(n) = Θ(Fib(n)) (tight bound)
如果您愿意,可以使用其闭式进一步减少紧密约束。
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
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)