第一章:动态规划算法入门:从斐波那契数列开始
动态规划(Dynamic Programming,简称 DP)是一种解决复杂问题的高效方法,尤其适用于那些可以分解为子问题,并且具有重叠子问题和最优子结构性质的问题。很多初学者觉得 DP 抽象、难以掌握,而本文将从最简单的例子——斐波那契数列——入手,带你一步步理解动态规划的本质,并与递归进行对比,揭开其神秘面纱。
一、什么是斐波那契数列?
斐波那契数列是一列这样的数:
F(0) = 0
F(1) = 1
F(n) = F(n - 1) + F(n - 2) (n >= 2)
这个数列的前几项是:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
斐波那契数列是动态规划学习的经典入门案例,因为它非常清楚地展现了递归带来的重复计算问题。
二、递归解法:直观但低效
我们可以用最直接的方式定义一个递归函数来求解斐波那契数列:
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
这种方式代码非常简洁,但它的时间复杂度是 O(2^n),随着 n 的增长,函数调用会呈指数级爆炸,原因是存在大量重复计算。
以 fib(5) 为例,它会递归调用 fib(4) 和 fib(3),而 fib(4) 又会调用 fib(3) 和 fib(2),如此往复。你会发现 fib(3) 被算了多次!
三、动态规划解法:高效且易优化
为了避免重复计算,我们可以使用动态规划思想,将中间结果保存起来。最常见的方式是“自底向上”的迭代方法。
3.1 使用数组保存结果(标准 DP)
function fib(n) {
if (n <= 1) return n;
const dp = new Array(n + 1);
dp[0] = 0;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
3.2 空间优化(滚动数组)
因为每一项只依赖前两项,我们其实不需要整个数组:
function fib(n) {
if (n <= 1) return n;
let a = 0,
b = 1;
for (let i = 2; i <= n; i++) {
let sum = a + b;
a = b;
b = sum;
}
return b;
}
- 时间复杂度:O(n)
- 空间复杂度:O(1)
四、动态规划与递归的比较
| 方法 | 时间复杂度 | 空间复杂度 | 是否重复子问题 | 是否保存中间结果 |
|---|---|---|---|---|
| 递归 | O(2^n) | O(n) | 是 | 否 |
| DP 数组 | O(n) | O(n) | 是 | 是 |
| DP 优化 | O(n) | O(1) | 是 | 是(滚动变量) |
递归虽然思路清晰,但效率太低。在大多数工程或算法面试场景中,动态规划才是可行的解决方案。
五、小结
通过斐波那契数列的例子,我们学习了动态规划的基本思想和应用过程:
- 识别子问题和重复计算
- 定义状态(如 dp[i])
- 写出状态转移方程(如 dp[i] = dp[i-1] + dp[i-2])
- 选择合适的数据结构保存中间结果
- 实现并优化代码
下一章,我们将通过“爬楼梯问题”进一步加深对动态规划的理解,它和斐波那契几乎是双胞胎!