算法入门必看:递归、记忆化搜索与动态规划的完整拆解

92 阅读4分钟

算法入门必看:递归、记忆化搜索与动态规划的完整拆解

前言

在刚接触算法时,我对递归、记忆化、动态规划这几个词一直有点混乱:
它们看起来很像,却又总是被分开讲;有时候觉得“我好像懂了”,一到做题却又不知道该用哪一个。

后来我才慢慢意识到:它们并不是三套割裂的技巧,而是同一类问题在不同阶段的解决方式
这篇文章就按照我自己的理解过程,用最常见的例子,把这条演进路径完整走一遍。

一、为什么要掌握递归与动态规划

可以把解题过程想成拼一幅复杂的拼图:

  • 递归:先把整幅图拆成很多小块,一次只专注解决一小块;
  • 记忆化:发现有些小块会反复出现,于是给已经拼好的部分贴上标签,下次直接用;
  • 动态规划(DP) :干脆把这些小块按顺序摆好,从最简单的开始,一块一块往上拼。

当一个问题同时出现:

  • 答案可以由更小规模的问题推导出来
  • 中间结果会被反复计算

那它基本就在向你暗示:这题值得往动态规划方向去想

二、递归入门:阶乘——递归的第一课

阶乘是我第一次真正“理解递归”的例子。

阶乘天然具有自相似结构:

n! = n × (n - 1)!

只要不断把问题规模缩小,最终一定会落到一个可以直接返回结果的情况。

function mul(n) {
  if (n === 1) return 1
  return n * mul(n - 1)
}

console.log(mul(6)) // 720

这里发生了什么?

  • 递推关系mul(n) = n * mul(n - 1)
  • 终止条件n === 1
  • 特点:每一层递归只计算一次,不存在重复子问题

所以在这个例子里,递归既直观又高效,非常合适。

三、递归的代价:斐波那契(Fibonacci)与重复计算的陷阱

递归真正的问题,往往出现在有重叠子问题的时候。

斐波那契数列定义如下:

f(n) = f(n - 1) + f(n - 2)

朴素递归实现

function fib(n) {
  if (n === 1 || n === 2) return 1
  return fib(n - 1) + fib(n - 2)
}

console.log(fib(10)) // 55

为什么它低效?

随着 n 变大,递归会不断向下分叉,
同一个 fib(k) 会在不同分支中被反复计算

结果是:看起来在拆问题,实际上在不停地重复劳动

时间复杂度接近指数级,n 稍微大一点就完全不可用。

四、记忆化与动态规划:爬楼梯(LeetCode 70)的三种思路

爬楼梯问题是我理解这三种思想关系的关键。

题意:

每次可以爬 1 或 2 阶,问到第 n 阶有多少种走法。

递推关系:

f(n) = f(n - 1) + f(n - 2)

本质上就是斐波那契的变体。

4.1朴素递归(不推荐)

代码很直观,但和斐波那契一样,会产生大量重复计算。
适合用来想清楚问题,不适合最终提交。

4.2记忆化递归(Top-down + cache)

在递归的基础上加一个缓存,把算过的结果存起来,下次直接取。

优点:

  • 思路接近递归,容易验证正确性
  • 解决了重复计算的问题

这一步,往往是**从“能跑”到“跑得动”**的关键过渡。

4.3动态规划(Bottom-up,推荐)

直接从最小子问题开始,一步步迭代构建答案。

var climbStairs = function(n) {
  if (n === 1) return 1
  const f = []
  f[1] = 1
  f[2] = 2
  for (let i = 3; i <= n; i++) {
    f[i] = f[i - 1] + f[i - 2]
  }
  return f[n]
}

console.log(climbStairs(5)) // 8

空间优化(滚动变量,O(1) 空间)

var climbStairs = function(n) {
  if (n === 1) return 1
  let a = 1, b = 2 // f(1), f(2)
  for (let i = 3; i <= n; i++) {
    const c = a + b
    a = b
    b = c
  }
  return b
}

这里本质上是在说: 当前状态只依赖前两个状态,那就没必要存整个数组。 e95acbcba91e9c0257291fb698842acb.jpg

总结

回过头看,递归、记忆化搜索与动态规划并不是彼此竞争的解法,而是一条逐步演进的学习路径:

递归让我们看清问题的结构;
记忆化让递归变得高效;
动态规划则把这种高效方式系统化、工程化。

在做题时,与其一开始就强行“套 DP 模板”,不如先写出递归,明确状态和边界,再判断是否存在重复子问题,最后自然过渡到动态规划。

一旦掌握这条思考路径,你会发现:无论是爬楼梯,还是更复杂的 DP 题目,真正难的从来不是代码,而是如何拆问题