LC509. 斐波那契数列
LC509. 斐波那契数
斐波那契数 (通常用 F(n)
表示)形成的序列称为 斐波那契数列 。该数列由
0 和 1
开始,后面的每一项数字都是前面两项数字的和。也就是:
1 | |
给定 n ,请计算 F(n) 。
示例 1:
1 | |
示例 2:
1 | |
示例 3:
1 | |
提示:
0 <= n <= 30
1. 带备忘录的递归解法
即然耗时的原因是重复计算,那么我们可以造一个「备忘录」;
每次遇到一个子问题先去「备忘录」里查一查,如果发现之前已经解决过这个问题了,直接把答案拿出来用,不要再耗时去计算了。
一般使用一个数组充当这个「备忘录」,当然你也可以使用哈希表(字典),思想都是一样的。
1 | |
实际上,带「备忘录」的递归算法,把一棵存在巨量冗余的递归树通过「剪枝」,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。

时空复杂度
递归算法的时间复杂度怎么计算?就是用子问题个数乘以解决一个子问题需要的时间。
子问题个数,即图中节点的总数,由于本算法不存在冗余计算,子问题就是
f(1), f(2), f(3) ...
f(20),数量和输入规模 n = 20 成正比,所以子问题个数为
O(n)。
解决一个子问题的时间,同上,没有什么循环,时间为 O(1)。
所以,本算法的时间复杂度是 O(n),比起暴力算法,是降维打击。
这段代码实现了一个带备忘录的递归算法来计算斐波那契数列的第
N 项。我们分别分析其时间复杂度和空间复杂度。
1.1 时间复杂度 (Time Complexity)
代码的核心部分是 dp
函数的递归调用。通过使用备忘录(memo
数组),我们可以避免重复计算,每个子问题的解只计算一次。
- 子问题数量:对于计算
fib(N),我们需要计算fib(0)到fib(N)共计N+1个子问题。 - 每个子问题的计算时间:每个子问题
fib(k)的计算时间可以认为是 O(1),因为只需要进行常数时间的加法操作。
因此,总的时间复杂度为子问题数量乘以每个子问题的计算时间: 时间复杂度:O(N)
1.2 空间复杂度 (Space Complexity)
空间复杂度主要考虑两个方面:递归调用栈和备忘录数组。
- 备忘录数组:我们使用了一个大小为
N+1的数组memo来存储中间结果,因此空间复杂度为 O(N)。 - 递归调用栈:在最坏情况下,递归调用的深度为
N(例如,从fib(N)递归到fib(0)),因此递归调用栈的空间复杂度也是 O(N)。
因此,总的空间复杂度为递归调用栈和备忘录数组的空间复杂度之和: 空间复杂度:O(N)
总结
- 时间复杂度:O(N)
- 空间复杂度:O(N)
这种带备忘录的递归算法被称为记忆化搜索(Memoization),它有效地将普通的指数时间复杂度优化为线性时间复杂度。
2. 最终优化解法
进一步优化,把空间复杂度降为 O(1)。这也就是我们最常见的计算斐波那契数的算法:
1 | |
好的,我们来分析一下这个计算斐波那契数列的代码的 时间复杂度 和 空间复杂度。
1. 时间复杂度分析
循环次数:
代码中的for循环从i = 2开始,直到i = n,总共运行了n - 1次。每次循环的操作:
每次循环中只进行了常数时间的操作(加法、赋值等),因此每次循环的时间复杂度是 (O(1))。总时间复杂度:
循环次数乘以每次循环的时间复杂度,即:
[ T(n) = (n - 1) O(1) = O(n) ]因此,时间复杂度为 (O(n))。
2. 空间复杂度分析
变量使用:
代码中只使用了固定的几个变量(dp_i_1、dp_i_2、dp_i、i),这些变量的空间占用是常数级别的。额外空间:
没有使用额外的数据结构(如数组、栈等),空间占用与输入规模 (n) 无关。总空间复杂度:
空间复杂度是 (O(1)),即常数空间复杂度。
总结
- 时间复杂度:(O(n))
- 空间复杂度:(O(1))
这个实现是一个典型的 动态规划优化版本,通过滚动数组的思想将空间复杂度从 (O(n)) 优化到了 (O(1)),同时保持了 (O(n)) 的时间复杂度。