序言
递归是算法设计中的利器,但如何避免重复计算和栈溢出?本文通过斐波那契数列和爬楼梯问题,带你掌握闭包记忆化和动态规划两大优化技巧。
递归的优雅与困境
递归是计算机科学中解决问题的经典方法,它将大问题分解为相似的小问题,直到达到退出条件。斐波那契数列就是一个绝佳的例子:
// 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
这个优化方案的精妙之处在于:
- 函数嵌套函数,形成闭包
- 内部函数可访问外部函数的自由变量
cache - 计算结果被缓存,避免重复计算
时间复杂度从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];
}
动态规划的核心思想:
- 自底向上:从基础情况开始构建
- 状态定义:
dp[i]表示i阶楼梯的爬法 - 状态转移方程:
dp[i] = dp[i-1] + dp[i-2] - 最优子结构:当前状态仅依赖前两个状态
空间优化版
注意到我们只需要前两个状态,可以进一步优化空间:
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) | 状态依赖有限前状态 |
在面试中遇到递归问题时:
- 先写基础递归解法,展示问题理解
- 分析复杂度问题(重复计算、栈溢出)
- 提出闭包优化方案,实现记忆化
- 建议动态规划,解决栈溢出风险
- 讨论空间优化可能,展示全面思考
结语
递归是算法设计的基石,而闭包记忆化和动态规划是优化递归的两大法宝。斐波那契和爬楼梯问题虽然简单,却包含了算法优化的核心思想:识别重复子问题、存储中间结果、重构计算顺序。
掌握这些技巧,不仅能解决具体问题,更能培养出面试官青睐的算法思维——从暴力解法出发,逐步优化,最终找到最优方案。