递归斐波那契算法的空间复杂度是多少?

Posted

技术标签:

【中文标题】递归斐波那契算法的空间复杂度是多少?【英文标题】:What is the space complexity of a recursive fibonacci algorithm? 【发布时间】:2015-04-29 15:10:37 【问题描述】:

这是Cracking the Coding Interview(第5版)中斐波那契数列的递归实现

int fibonacci(int i) 
       if(i == 0) return 0;
       if(i == 1) return 1;
       return fibonacci(i-1) + fibonaci(i-2);

看了这个算法时间复杂度的视频Fibonacci Time Complexity,我现在明白了为什么这个算法运行在O(2n)。但是,我正在努力分析空间复杂性。

我上网查了一下,对此有疑问。

在这个 Quora 帖子中,作者指出“在你的情况下,你有 n 个堆栈帧 f(n)、f(n-1)、f(n-2)、...、f(1) 和奥(1)”。你不会有 2n 个堆栈帧吗?说 f(n-2) 一帧将用于实际调用 f(n-2) 但不会还有来自 f(n-1) 的调用 f(n-2) 吗?

【问题讨论】:

常数因素在大 O 复杂度中并不重要—— O(n) 和 O(2n) 是相同的。也就是说,在第一次调用返回后,堆栈帧将被回收并重用于第二次调用。 这是 LaTeX 数学符号,2 的 n 次方吗?你的意思是 2 倍 n 吗? @chrislott 你能对数学 jax 进行编辑吗?我的意思是 2 的幂 n @ChrisDodd 所以在计算机中,有一个 f(n-2) 的堆栈帧,并且因为有两次调用而被使用了两次? @committedandroider:您可以编辑自己的帖子。 *** 上没有 MathJax,所以你必须用 html 来做。 【参考方案1】:

在我看来,这个过程一次只会下降一个递归。 第一个 (f(i-1)) 将创建 N 个堆栈帧,另一个 (f(i-2)) 将创建 N/2。 所以最大的是N。另一个递归分支不会占用更多空间。

所以我会说空间复杂度是 N。

由于 f(i-2) 小于 f(i-1) 空间,因此一次只计算一个递归,这一事实允许忽略它。

【讨论】:

哦,是不是因为另一个 f(i-2) 会等待第一个 f(i-1) 完成? @committedandroider。是的,在每个级别,所有 f(i-1) 都在 f(i-2) 开始之前执行。请注意,较低级别的 f(i-2) 被评估为下一个较高级别 f(i-1) 的一部分。【参考方案2】:

这里有一个提示。使用 print 语句修改您的代码,如下例所示:

int fibonacci(int i, int stack) 
    printf("Fib: %d, %d\n", i, stack);
    if (i == 0) return 0;
    if (i == 1) return 1;
    return fibonacci(i - 1, stack + 1) + fibonacci(i - 2, stack + 1);

现在在 main 中执行这一行:

Fibonacci(6,1);

打印出的“堆栈”的最高值是多少。你会看到它是“6”。尝试“i”的其他值,您会发现打印的“stack”值永远不会超过传入的原始“i”值。

由于 Fib(i-1) 在 Fib(i-2) 之前被完全评估,所以递归级别永远不会超过 i

因此,O(N)。

【讨论】:

那个栈变量有什么意义? @committedandroider 我通常称它为depth 而不是stack。它跟踪递归级别的数量,以便printf 可以显示它。 (您也可以使用它来限制递归深度,但这里不需要。) 空间复杂度更多地取决于递归级别还是堆栈帧数? @committedandroider 每一层递归都有一个栈帧,所以使用的空间等于“最深的递归层”乘以“栈的大小”每个级别的框架”。 空间复杂度只是“最深层次的递归”,因为每层的堆栈帧大小是一个常数乘数,因此在复杂度分析中被忽略。 @committedandroider 是和否。 f(3,2) 实例将设置一个堆栈帧。在f(3,2) 返回后,堆栈内存被回收。对f(2,2) 的调用将在内存中的同一位置设置不同的堆栈帧。所以他们是否使用相同的堆栈框架取决于您如何定义“相同的堆栈框架”。【参考方案3】:

递归实现的近似时间复杂度为 2 平方 n (2^n),这意味着该算法必须经过大约 64 个计算步骤才能获得第 6 个斐波那契数。这是巨大的并且不是最优的,大约需要程序 1073741824 的计算步骤才能得到 30 的斐波那契数,这是一个很小的数字,这是不可接受的。实际上,迭代方法肯定要好得多且经过优化。

知道了这个实现的时间复杂度之后,你可能会认为它的空间复杂度可能是一样的,其实不然。此实现的空间复杂度等于 O(n),并且永远不会超过它。那么,让我们揭开“为什么”的神秘面纱?

这是因为递归执行的函数调用乍一看似乎是并发执行的,但实际上它们是按顺序执行的。

顺序执行保证堆栈大小永远不会超过上述调用树的深度。程序首先执行所有最左边的调用,然后向右转,当 F0 或 F1 调用返回时,它们对应的堆栈帧被弹出。

在下图中,每个矩形代表一个函数调用,箭头代表程序到达递归结束所经过的路径:

这里我们可以注意到,当程序到达调用F4F3F2F1并返回1时,它又回到它的父调用F4F3F2执行右递归子调用F4F3F2F0,当F4F3F2F0和F4F3F2F1调用都返回时,程序返回到 F4F3。所以栈永远不会超过最长路径F4F3F2F1的大小。

程序将遵循同样的执行模式,从父节点到子节点,在执行左右调用后返回父节点,直到到达所有 F4 的最后一个根父节点,得到计算得出的斐波那契之和为 3。

【讨论】:

这个答案应该被接受恕我直言

以上是关于递归斐波那契算法的空间复杂度是多少?的主要内容,如果未能解决你的问题,请参考以下文章

[算法学习]斐波那契数计算

《剑指offer》---斐波那契数列

剑指offer

斐波那契数与二分法的递归与非递归算法及其复杂度分析

斐波那契高效算法(4种算法综合分析)

算法系列 -- 递归/递推优化