从爬楼梯到算斐波那契,我终于弄懂了递归和动态规划这俩 "磨人精"
最近在代码界摸爬滚打,总被两个词按在地上摩擦 —— 递归和动态规划。这俩货就像数学题里的 "小明",天天换着花样折磨人,今天就来好好扒一扒它们的底裤。
递归:自己调自己的 "套娃大师"
递归这东西,说白了就是函数自己喊自己的名字。就像小时候问妈妈 "我从哪来的",妈妈说 "你是妈妈生的",再问 "妈妈从哪来的",妈妈说 "外婆生的"... 一直问到祖宗十八代,这就是递归的精髓 —— 找规律 + 找出口。
比如算个阶乘,用递归(时间复杂度过高)写出来是这样:
function mul(n){
if(n === 1){ // 出口:问到祖宗了
return 1
}
return n * mul(n-1)
}
这代码简洁得像诗,但算个斐波那契数列就露馅了。那个 1,1,2,3,5,8... 的数列,递归写法看着简单:
递归的 "中年危机":重复计算让 CPU 原地冒烟
function fb(n){
if(n === 1 || n === 2){
return 1
}
return fb(n-1) + fb(n-2)
}
这代码算个 n=10 还行,要是算 n=40,能让你喝杯咖啡回来还没出结果。就像你查快递单号,每次都要从快递员刚取件的时候查起,哪怕昨天刚查过。
给递归装个 "备忘录":记忆化搜索救场
比如爬楼梯问题:一次能爬 1 或 2 阶,到第 n 阶有几种走法?
后来我灵机一动,给递归加了个小本本(数组 f):
const f = []
var climbStairs = function (n) {
if (n === 1 || n === 2) {
return n
}
if (f[n] === undefined) { // 查小本本,没记过才计算
f[n] = climbStairs(n - 1) + climbStairs(n - 2)
}
return f[n]
};
这招叫 "记忆化搜索"(提效),相当于把算过的结果记在通讯录里,下次直接拨号不用重新查号。
后来才发现,这代码就像给老年机装了智能手机的通讯录 —— 思路对但效率不够。全局数组 f 在多组测试用例下会残留历史数据,而且递归调用本身就有函数栈的开销,n 太大时还是扛不住。
彻底换个活法:动态规划的 "自底向上" 哲学
- 站在已知的角度,通过已知来定位未知
最后改用
纯动态规划找到动态方程)写法,直接逆袭:
var climbStairs = function (n){
const f = []
// 先搞定已知的1楼和2楼
f[1] = 1
f[2] = 2
// 从3楼开始往上爬,每步都踩在前人的肩膀上
for(let i = 3;i<=n;i++){
f[i] = f[i-1] + f[i-2]
}
return f[n]
}
这思路就像盖楼,从 1 层开始一层层往上盖,每一层的建材都直接用前两层的,根本不用回头看。没有递归的函数调用开销,也没有重复计算,效率直接拉满。
总结:三种写法的生存现状
| 写法 | 特点 | 适合场景 |
|---|---|---|
| 纯递归 | 代码简洁如诗 | 理解思路用,n≤30 |
| 记忆化搜索 | 加了缓存的递归 | 教学演示,n≤1000 |
| 动态规划 | 自底向上迭代 | 实际开发,n多大都不怕 |
总结:什么时候该套娃,什么时候该记笔记?
-
递归适合简单问题或调试时用,写起来爽,但容易重复劳动
-
动态规划适合复杂问题,虽然前期要多写几行,但跑起来飞快
-
记住:所有动态规划问题,
先建个空数组当小本本准没错
现在终于明白,递归是浪漫的诗人,只顾优雅不管效率; 动态规划是务实的会计,每一笔账都记得清清楚楚。 下次再遇到这俩货,我可不会再被它们忽悠了!