第一章:动态规划算法入门:从斐波那契数列开始

96 阅读3分钟

第一章:动态规划算法入门:从斐波那契数列开始

动态规划(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)是(滚动变量)

递归虽然思路清晰,但效率太低。在大多数工程或算法面试场景中,动态规划才是可行的解决方案

五、小结

通过斐波那契数列的例子,我们学习了动态规划的基本思想和应用过程:

  1. 识别子问题和重复计算
  2. 定义状态(如 dp[i])
  3. 写出状态转移方程(如 dp[i] = dp[i-1] + dp[i-2])
  4. 选择合适的数据结构保存中间结果
  5. 实现并优化代码

下一章,我们将通过“爬楼梯问题”进一步加深对动态规划的理解,它和斐波那契几乎是双胞胎!