方法一:递归(我的第一想法)

如何计算递归的时间复杂度?
- 首先计算子问题个数:递归中的子问题个数即为递归树中节点的总数。二叉树的节点总数为指数级别,所以子问题个数为O(2n)
- 然后计算解决一个子问题的时间:在本算法中没有循环,只有一个加法操作,所以时间为O(1)
- 综上,这个算法的时间复杂度为二者相乘,为O(2n)
递归算法的缺点
- 这个算法的递归树存在大量的重复节点(比如两个f(18)节点),即会进行大量的重复计算,增加了运算时间。
- 重叠子问题就体现为递归树中有大量重复节点,可以考虑用动态规划方法解决。
递归解法的时间和空间复杂度
/*
* @Author: LibraStalker **********
* @Date: 2023-03-25 16:19:58
* @FilePath: BM62-斐波那契数列.js
* @Description:
*/
function Fibonacci(n) {
// write code here
if (n === 1 || n === 2) {
return 1;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
module.exports = {
Fibonacci: Fibonacci,
};
方法二:记忆化搜索(带备忘录的递归解法)

带备忘录的递归解法的优点
- 直接递归会存在许多重复计算,一般使用map或者array等数据结构充当备忘录,保存子问题的结果,后续如果有重复子问题可以直接拿备忘录中的结果,避免了重复计算。
- 带备忘录的递归算法,把一棵存在巨量冗余的递归树通过剪枝,改造成了一幅不存在冗余的递归图,极大减少了子问题(即递归图中节点)的个数。
带备忘录的递归解法和迭代的动态规划解法的区别
- 带备忘录的递归解法的效率已经和迭代的动态规划解法一样了。实际上,这种解法和常见的动态规划解法已经差不多了,但是带备忘录的递归解法增加了备忘录,会增加空间复杂度。
- 带备忘录的递归解法是自顶向下进行递归求解,我们更常见的动态规划代码是自底向上进行递推求解。
自顶向上递归解法和自底向下递推解法
- 自顶向上递归解法从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1) 和 f(2) 这两个 base case,然后逐层返回答案,这就叫自顶向下。
- 自底向下递推解法从最底下、最简单、问题规模最小、已知结果的 f(1) 和 f(2)(base case)开始往上推,直到推到我们想要的答案 f(20)。这就是递推的思路,这也是动态规划一般都脱离了递归,而是由循环迭代完成计算的原因。
带备忘录的递归解法的时间和空间复杂度
- 时间复杂度:O(n)(因为没有重复的节点,子问题的个数就是输入规模n,所以子问题个数为O(n),子问题规模还是O(1),两者相乘得到O(n))
- 空间复杂度:O(n)和递归栈的空间
/*
* @Author: LibraStalker **********
* @Date: 2023-03-25 16:19:58
* @FilePath: BM62-斐波那契数列.js
* @Description:
*/
const result = new Array(50).fill(0);
function Fibonacci(n) {
// write code here
if (n === 1 || n === 2) {
result[n] = 1;
return result[n];
}
if (result[n] > 0) {
return result[n];
}
result[n] = Fibonacci(n - 1) + Fibonacci(n - 2);
return result[n];
}
module.exports = {
Fibonacci: Fibonacci,
};
方法三:动态规划-DP数组

DP数组和备忘录的关系
- DP数组和带备忘录的递归解法中的备忘录类似,就是记录了各个子问题的结果。带备忘录的递归解法中的备忘录,最终完成后就是这个DP数组。
- DP数组的动态规划解法的效率和带备忘录的递归解法效率基本相同。
状态转移方程

- 状态转移方程实际上就是描述问题结构的数学形式,是解决问题的核心,其实状态转移方程直接代表着暴力解法。
- f(n) 的函数参数会不断变化,所以你把参数 n 想做一个状态,这个状态 n 是由状态 n - 1 和状态 n - 2 转移(相加)而来,这就叫状态转移。
- 带备忘录的递归解法和动态规划的DP数组解法都是围绕这个方程式的不同表现形式。
动态规划的DP数组解法的时间和空间复杂度
/*
* @Author: LibraStalker **********
* @Date: 2023-03-25 16:19:58
* @FilePath: BM62-斐波那契数列.js
* @Description:
*/
function Fibonacci(n) {
const dpResult = new Array(41).fill(0);
dpResult[1] = 1;
dpResult[2] = 1;
for (let i = 3; i <= n; ++i) {
dpResult[i] = dpResult[i - 1] + dpResult[i - 2];
}
return dpResult[n];
}
module.exports = {
Fibonacci: Fibonacci,
};
方法四:动态规划-缩减DP数组
DP数组的优化
- 动态规划的DP数组中保存着所有状态的结果,但是有时状态转移只需要DP数组中的一部分状态,则可以缩小DP数组的大小,只记录必要的数据,从而降低空间复杂度。
- 斐波那契数列问题中当前状态只和之前的两个状态有关,因此不需要那么长的DP数组存储所有状态,只要想办法存储之前的两个状态就行了。
动态规划问题的三个特征

- 重复子问题:重复子问题可以从递归树中的重复节点体现出来,斐波那契数列问题旨在说明重叠子问题的消除方法,演示得到最优解法逐步求精的过程。
- 无后效性:当前状态不会受到后面状态的影响,只跟前面的状态有关。
- 最优子结构:后面的状态可以通过前面的状态推导出来。
动态规划的缩减DP数组解法的时间和空间复杂度
/*
* @Author: LibraStalker **********
* @Date: 2023-03-25 16:19:58
* @FilePath: BM62-斐波那契数列.js
* @Description:
*/
function Fibonacci(n) {
if (n === 1 || n === 2) {
return 1;
}
let dp_i_1 = 1;
let dp_i_2 = 1;
let dp_i;
for (let i = 3; i <= n; ++i) {
dp_i = dp_i_1 + dp_i_2;
dp_i_1 = dp_i_2;
dp_i_2 = dp_i;
}
return dp_i;
}
module.exports = {
Fibonacci: Fibonacci,
};