递归、闭包与动态规划的奇妙冒险:从斐波那契到爬楼梯

126 阅读6分钟

今天,我的大脑和JavaScript引擎进行了一场深度对话,主题是:如何优雅地让计算机学会"数楼梯"和"养兔子"。

第一章:递归的美丽与哀愁

想象一下,你站在一栋高楼的楼梯前,每次只能迈1阶或2阶。爬到楼顶有多少种走法?或者换个场景:一对兔子每月生一对小兔,小兔两个月后又能生育,一年后会有多少兔子?这两个看似不相关的问题,在代码世界里竟有着相同的基因序列!

// 斐波那契的递归舞步
function fib(n) {
    if (n <= 1) return n; // 终点站:0阶或1阶
    return fib(n - 1) + fib(n - 2); // 分身为两个自己
}
console.log(fib(10)); // 输出:55

这段优雅的代码背后藏着一个秘密:树状分身术!当我们计算fib(10)时,它会分裂成fib(9)和fib(8),每个子问题继续分裂,直到触达基础条件。就像细胞分裂:

        f(10)
    f(9)      f(8)
 f(8)  f(7)  f(7)  f(6)  // 可以很清楚地看到重复的计算
            ···
        f(0)  f(1)

美中不足的是——当n=40时,函数调用次数会超过2亿次!这就像让蚂蚁数清撒哈拉沙粒,可怜的JavaScript引擎会举白旗投降:"RangeError: Maximum call stack size exceeded"。这就出现了我们的爆栈,也就是栈溢出的情况,我们知道,函数在执行栈里面执行,先入后出,只有当最后进入栈的函数执行完毕才会出栈,之后陆续出栈,递归可能就会造成栈满,导致栈溢出的情况

第二章:闭包的记忆魔法

如何拯救濒临崩溃的引擎?答案藏在闭包这个时间管理大师的口袋里:

function memoizedFib() {
    const cache = {}; // 记忆宝盒
    
    return function fib(n) {
        if (n <= 1) return n;
        if (cache[n]) return cache[n]; // 查历史记录
        
        cache[n] = fib(n - 1) + fib(n - 2); // 存档新结果
        return cache[n];
    }
}

const smartFib = memoizedFib();
console.log(smartFib(100)); // 秒算354224848179262000000

闭包的精妙之处在于:

  1. 函数嵌套函数:外层是保险箱(cache),内层是金库管理员(fib)
  2. 自由变量永生:cache在函数执行后依然存活,就像永远在线的云备忘录
  3. 空间换时间:用O(n)空间把时间复杂度从O(2^n)降到O(n)

试试调用smartFib(10)时的记忆轨迹:

cache = {
  2: 1,
  3: 2,
  4: 3,
  5: 5,
  ... // 像滚雪球般积累
}

第三章:全局变量的双刃剑

闭包虽好,但有些场景需要更直接的记忆方式。(当然这里我们依旧可以用闭包的方式)比如爬楼梯问题:

// 原始递归版:优雅但脆弱
const climbStairs = function (n) {
    if (n === 1) return 1; // 1阶:1种走法
    if (n === 2) return 2; // 2阶:2种走法(1+1或2)
    return climbStairs(n-1) + climbStairs(n-2);
}

这里我们同样存在栈溢出的问题

给这个递归战士配上全局记忆铠甲:

const f = []; // 全局记忆阵列
const climbStairs = function (n) {
    if (n == 1) return 1;
    if (n == 2) return 2;
    
    if (f[n] === undefined) 
        f[n] = climbStairs(n-1) + climbStairs(n-2);
    
    return f[n]; // 返回记忆中的答案
}
console.log(climbStairs(100)); // 573147844013817200000

但全局变量是带刺的玫瑰:

  • ✅ 优点:简单粗暴,跨函数共享
  • ❌ 致命伤:可能被意外修改(想象队友写了句f = []这种感觉就像你打王者碰到🐖队友,把你打野红蓝buff抢了

第四章:动态规划的降维打击

真正的终极大招来了——动态规划(DP) !它把递归倒过来思考,从地基开始建楼:

const climbStairs = function (n) {
    const dp = []; // DP数组:dp[i]表示i阶楼梯的走法
    dp[1] = 1; // 1阶:1种
    dp[2] = 2; // 2阶:2种
    
    // 自底向上建造
    for (let i = 3; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2]; // 状态转移方程
    }
    
    return dp[n]; // 返回顶层设计
}

从底层开始,知道每层阶梯的情况

dp[3]=3
dp[4]=dp[3]+dp[2]=5
dp[5]=dp[4]+dp[3]=8
·····
dp[n]=dp[n-1]+dp[n-2]

我们是利用循环来解决,并没有栈溢出的情况,不会消耗过多的内存

DP的智慧体现在:

  1. 最优子结构:当前解=子问题解的组合
  2. 无后效性:未来决策不影响历史状态
  3. 空间换时间:O(n)时间+O(n)空间

更妙的是空间压缩技巧(滚动数组):

const climbStairs = (n) => {
    if (n <= 2) return n;
    let a = 1, b = 2;
    for (let i = 3; i <= n; i++) {
        [a, b] = [b, a + b]; // 数组解构,双指针交替前进
    }
    return b;
}
// 空间复杂度降至O(1)!

✅ 运行过程示例(n = 5):

步骤ia (前前项)b (前一项)新 b = a + b
初始-123
i=3321+2=35
i=4432+3=58
i=5553+5=8-

最终返回 b = 8,即 f(5)

✅ 优点总结

优点描述
空间最优只使用常量级空间 O(1),适用于内存受限场景
无递归栈溢出风险是迭代而非递归,不会爆栈
高效简洁没有多余的条件判断或数据结构操作
可扩展性强若题目变为每次能爬 1、2、3 阶,也能轻松改成三变量滚动

第五章:面试官的思维解码器

"大问题拆分小问题(相似)→ 退出条件 → 避免重复计算 → 防止爆栈"

这四步法就像编程世界的黄金分割:

  1. 自顶向下设计(人类思考方式)

    • 爬n阶楼梯 = 先走1阶 + 爬(n-1)阶,或先走2阶 + 爬(n-2)阶
  2. 自底向上实现(机器执行方式)

    • 从dp[1]和dp[2]开始迭代

递归与DP的本质区别

特性递归动态规划
思考方向自顶向下自底向上
计算方式可能重复计算无重复
空间复杂度调用栈O(n)通常O(n)
时间复杂度指数级(O(2^n))线性(O(n))
适用场景问题规模小大规模问题

结语:编程之道的三重境界

  1. 见山是山:暴力递归直抒胸臆
  2. 见山不是山:闭包/全局变量优化避免重复
  3. 见山还是山:动态规划直击本质

下次当你看到斐波那契数列时,请记得:

  • 兔子繁殖问题是13世纪斐波那契的数学游戏
  • 爬楼梯解法出现在1995年《算法导论》经典案例
  • 闭包记忆法则是JavaScript的函数式魔法

最后送上代码哲学三连:

// 初学者
const fib1 = n => n <= 1 ? n : fib1(n-1) + fib1(n-2);

// 进阶者
const fib2 = (() => {
    const cache = [0,1];
    return n => cache[n] ?? (cache[n] = fib2(n-1) + fib2(n-2));
})();

// 悟道者
const climbStairs = function (n) {
    const dp = []; // DP数组:dp[i]表示i阶楼梯的走法
    dp[1] = 1; // 1阶:1种
    dp[2] = 2; // 2阶:2种
    
    // 自底向上建造
    for (let i = 3; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2]; // 状态转移方程
    }
    
    return dp[n]; // 返回顶层设计
}

// 成道者
const fib3 = n => {
    let [a, b] = [0, 1];
    for (let i = 0; i < n; i++) [a, b] = [b, a + b];
    return a;
};

记住:好的代码像诗歌,既要优雅简洁,也要经得起大规模数据的考验!