递归、动态规划与记忆化搜索:初学者的自我修养

120 阅读4分钟

“递归是什么?就是函数自己调自己,调着调着就把自己绕晕了。”——一位刚学递归的前端小白

一、递归到底是个啥?

递归,听起来高大上,其实就是一个函数在自己体内又调了自己。就像你问镜子里的自己:“你是谁?”镜子里的你又问:“你是谁?”……无限套娃,直到你妈喊你吃饭。

递归的两大法宝

  1. 找规律:每次递归都要做点啥,不能白调。
  2. 找终止条件:总得有个出口,不然就死循环了。

举个例子,计算5的阶乘(5! = 5×4×3×2×1):

function mul(n) {
    if (n === 1) {
        return 1
    }
    return n * mul(n-1)
}
console.log(mul(5)); // 输出120
  • 规律:n! = n × (n-1)!
  • 终止条件:n === 1

递归的本质,就是把大问题拆成小问题,一步步递归下去,直到遇到最小的“原子问题”直接返回。

二、递归的“亲兄弟”——斐波那契数列

斐波那契数列,传说中兔子繁殖问题的鼻祖。数列长这样:1, 1, 2, 3, 5, 8, 13, 21, ...

每一项等于前两项之和。用递归写法:

function fib(n) {
    if (n === 1 || n === 2) {
        return 1
    }
    return fib(n-1) + fib(n-2)
}
console.log(fib(10)); // 输出55
  • 规律:fib(n) = fib(n-1) + fib(n-2)
  • 终止条件:n === 1 或 n === 2

看起来很优雅对吧?但你要是算fib(50),电脑可能要“烧脑”半天,因为递归会重复计算很多次同样的子问题。

三、递归的“效率提升神器”——记忆化搜索

递归虽然优雅,但有个致命缺点:重复劳动。比如fib(10)会反复算fib(5)、fib(4)……,就像你妈一天问你十遍“吃饭没”。

怎么办?用记忆化搜索!就是把已经算过的结果存起来,下次再用直接查表,效率嗖嗖提升。

伪代码如下:

const memo = []
function fib(n) {
    if (n === 1 || n === 2) return 1
    if (memo[n]) return memo[n]
    memo[n] = fib(n-1) + fib(n-2)
    return memo[n]
}

这样,每个子问题只算一次,妈妈再也不用担心我的CPU过热了!

四、动态规划:递归的“进化版”

动态规划(Dynamic Programming,简称DP),其实就是递归的“倒着推”版本。递归是“自顶向下”,DP是“自底向上”。

以爬楼梯问题为例:每次可以爬1级或2级,问爬到n级有多少种方法?

递归思路

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

动态规划思路

我们可以先算出1级、2级的结果,然后一步步推到n级:

var climbStairs = function(n) {
    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]
};
  • f[1] = 1(一级楼梯只有一种爬法)
  • f[2] = 2(两级楼梯有两种爬法:1+1 或 2)
  • f[n] = f[n-1] + f[n-2](每次可以从前一级或前两级跳上来)

动态规划的精髓:把大问题拆成小问题,先解决小问题,再一步步推到大问题。

五、递归、记忆化、动态规划的区别与联系

  • 递归:思路清晰,代码优雅,但容易重复计算,效率低。
  • 记忆化搜索:递归+缓存,避免重复劳动,效率高。
  • 动态规划:自底向上,空间换时间,效率最高。

三者的关系就像是“原始人→农耕人→现代人”的进化史。

六、初学者常见的“递归坑”

  1. 忘记终止条件:没有出口,递归会一直调下去,直到爆栈。
  2. 重复计算:没用记忆化,效率低下。
  3. 思路混乱:递归和DP傻傻分不清楚。

七、递归的“脑洞时刻”

递归不仅能算阶乘、斐波那契、爬楼梯,还能用来解决树的遍历、全排列、八皇后等一系列“烧脑”问题。只要你敢想,递归就能帮你“套娃”到底!

八、总结

递归其实并不难,难的是如何把问题拆解成递归子问题,并且找到合适的终止条件。记忆化和动态规划则是递归的“效率外挂”,让你写出既优雅又高效的代码。

“递归的世界很美好,只要你不怕头晕!”
“动态规划的世界很高效,只要你不怕推公式!”

希望这篇文章能让你对递归、动态规划和记忆化搜索有一个通俗又深刻的理解,成为算法世界的“套娃大师”!