递归优化之道:从斐波那契到爬楼梯的闭包与动态规划实践

262 阅读4分钟

序言

递归是算法设计中的利器,但如何避免重复计算和栈溢出?本文通过斐波那契数列和爬楼梯问题,带你掌握闭包记忆化和动态规划两大优化技巧。

递归的优雅与困境

递归是计算机科学中解决问题的经典方法,它将大问题分解为相似的小问题,直到达到退出条件。斐波那契数列就是一个绝佳的例子:

// 1.js
function fib(n) {
  if (n <= 1) return n; // 退出条件
  return fib(n - 1) + fib(n - 2); // 分解问题
}

这种解法虽然优雅直观,但存在严重问题——重复计算。当我们计算fib(10)时,函数调用树如下:

              fib(10)
            /          \
        fib(9)        fib(8)
        /     \        /    \
    fib(8) fib(7)  fib(7) fib(6)

可以看到fib(8)fib(7)等被重复计算多次。时间复杂度高达O(2ⁿ),计算fib(50)就需要约1.12e+15次操作!

闭包优化:记忆化递归

解决重复计算的利器是闭包。闭包可以捕获并持久化自由变量,实现计算结果缓存:

// 2.js
function memorizeFib() {
  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 fib = memorizeFib();
console.log(fib(100)); // 秒算354224848179262000000

这个优化方案的精妙之处在于:

  1. 函数嵌套函数,形成闭包
  2. 内部函数可访问外部函数的自由变量cache
  3. 计算结果被缓存,避免重复计算

时间复杂度从O(2ⁿ)优化到O(n),空间复杂度O(n),实现了质的飞跃!

爬楼梯问题:递归的另一个视角

爬楼梯问题是另一个经典递归场景:每次可以爬1或2阶,n阶楼梯有多少种爬法?

// 3.js
const climbStairs = function(n) {
  if (n == 1) return 1; // 1阶:1种方式
  if (n == 2) return 2; // 2阶:2种方式
  return climbStairs(n - 1) + climbStairs(n - 2); // 递归分解
}

这实际上就是斐波那契数列的变种!同样面临重复计算问题。

闭包优化爬楼梯

我们可以用类似的闭包技巧优化:

// 4.js(修正版)
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

动态规划:自底向上的迭代解法

虽然闭包优化解决了重复计算,但递归仍存在函数调用栈溢出的风险。动态规划提供了更优解:

// 5.js
const climbStairs = function(n) {
  const dp = []; // DP数组
  dp[1] = 1; // 初始状态
  dp[2] = 2;
  
  for (let i = 3; i <= n; i++) {
    dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
  }
  
  return dp[n];
}

动态规划的核心思想:

  1. 自底向上:从基础情况开始构建
  2. 状态定义dp[i]表示i阶楼梯的爬法
  3. 状态转移方程dp[i] = dp[i-1] + dp[i-2]
  4. 最优子结构:当前状态仅依赖前两个状态

空间优化版

注意到我们只需要前两个状态,可以进一步优化空间:

const climbStairs = function(n) {
  if (n <= 2) return n;
  
  let prev = 1; // dp[i-2]
  let curr = 2; // dp[i-1]
  
  for (let i = 3; i <= n; i++) {
    const next = prev + curr;
    prev = curr;
    curr = next;
  }
  
  return curr;
}

空间复杂度从O(n)优化到O(1),同时避免了递归调用栈风险!

面试思维总结

优化技巧解决痛点时间复杂度空间复杂度适用场景
基础递归-O(2ⁿ)O(n)小规模问题
闭包记忆化重复计算O(n)O(n)递归结构清晰
动态规划重复计算+栈溢出O(n)O(n)有最优子结构
动态规划(优化)空间消耗O(n)O(1)状态依赖有限前状态

在面试中遇到递归问题时:

  1. 先写基础递归解法,展示问题理解
  2. 分析复杂度问题(重复计算、栈溢出)
  3. 提出闭包优化方案,实现记忆化
  4. 建议动态规划,解决栈溢出风险
  5. 讨论空间优化可能,展示全面思考

结语

递归是算法设计的基石,而闭包记忆化和动态规划是优化递归的两大法宝。斐波那契和爬楼梯问题虽然简单,却包含了算法优化的核心思想:识别重复子问题、存储中间结果、重构计算顺序

掌握这些技巧,不仅能解决具体问题,更能培养出面试官青睐的算法思维——从暴力解法出发,逐步优化,最终找到最优方案。