面试官:小伙子,写个斐波那契数列吧,简单的很!
我:那必须安排!(心想:这还不是手到擒来)
面试官:嗯,不错,那你再优化一下性能?
我:......(开始冒汗)
相信很多同学都遇到过类似的场景。今天我们就来聊聊递归优化这个老生常谈却又常考常新的话题,从最基础的递归实现到高级优化技巧,带你领略算法优化的魅力!
第一境界:天真的递归 😅
让我们从最经典的斐波那契数列开始:
// 递归的基本要素
// 1. 相似的问题
// 2. 自顶向下的思考
// 3. 明确问题的终点(退出条件)
// 4. 注意重复计算问题
// 5. 树状结构分析
function fib(n) {
if (n <= 1) return n; // 退出条件
return fib(n - 1) + fib(n - 2); // 递归调用
}
console.log(fib(10)); // 55
这个实现看起来很优雅,但是有个致命问题——重复计算!
我们来分析一下 fib(10) 的调用树:
f(10)
f(9) f(8)
f(8) f(7) f(7) f(6)
...
可以看到,f(8)、f(7) 等被重复计算多次。时间复杂度达到了恐怖的 O(2^n) !
第二境界:闭包优化记忆化 🧠
面试官最爱考的就是这个优化思路——用闭包实现记忆化:
// 如何用闭包优化fib
// 核心思想:给函数添加记忆能力
function memoizedFib(){
// 闭包的两个关键要素:
// 1. 函数嵌套函数
// 2. 自由变量(外层变量被内层函数引用)
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 = memoizedFib();
console.log(fib(100)); // 瞬间出结果!
这里用到了闭包的核心特性:
- 内层函数可以访问外层函数的变量(
cache) - 外层函数执行完毕后,
cache变量依然被保持在内存中 - 每次调用
fib都能访问到同一个cache对象
时间复杂度从 O(2^n) 优化到了 O(n) !
实战应用:爬楼梯问题 🪜
让我们把这个思路应用到经典的爬楼梯问题上:
问题描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
暴力递归版本
// 自顶向下 f(n) -> f(n-1) + f(n-2)
// 画树形结构分析(方程不明显,有利于推导)
// 问题:重复计算,函数入栈太多
const climbStairs = function (n) {
if (n == 1) return 1;
if (n == 2) return 2;
return climbStairs(n-1) + climbStairs(n-2);
}
console.log(climbStairs(100)); // 会卡死!
记忆化递归版本
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)); // 秒出结果
第三境界:动态规划——自底向上 📈
虽然记忆化递归解决了重复计算问题,但还有一个隐患——调用栈过深可能导致栈溢出。
让我们换个思路,自底向上思考:
// 递归是自顶向下,动态规划是自底向上
// 从 f(1) = 1, f(2) = 2 开始,逐步计算到 f(n)
// 优势:迭代方式,不需要函数入栈,也不需要额外的递归空间
const climbStairs = function(n){
// dp 数组存储子问题的解
const 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] = dp[i-1] + dp[i-2]
性能对比 📊
让我们来看看三种方法的性能对比:
| 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
| 暴力递归 | O(2^n) | O(n) | 简单直观,但性能极差 |
| 记忆化递归 | O(n) | O(n) | 性能优秀,但可能栈溢出 |
| 动态规划 | O(n) | O(n) | 性能优秀,空间可进一步优化 |
进阶优化:空间优化 🎯
动态规划还能进一步优化空间复杂度:
const climbStairs = function(n) {
if (n <= 2) return n;
let prev2 = 1; // dp[i-2]
let prev1 = 2; // dp[i-1]
for (let i = 3; i <= n; i++) {
const current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
空间复杂度从 O(n) 优化到了 O(1) !
面试官的考察思维 💡
通过这道题,面试官想考察的知识点包括:
基础能力
- 递归思维:能否将大问题拆解为相似的小问题
- 边界条件:是否能正确处理退出条件
- 复杂度分析:是否能分析算法的时间和空间复杂度
进阶能力
- 闭包理解:能否运用闭包实现函数记忆化
- 优化思维:是否具备从递归到动态规划的优化思路
- 工程思维:在实际项目中如何权衡性能和可读性
架构思维
- 自顶向下 vs 自底向上:两种不同的思维模式
- 空间换时间:缓存策略在实际开发中的应用
- 函数式编程:闭包、高阶函数等概念的实际运用
总结 🎉
从斐波那契到爬楼梯,我们经历了递归优化的三重境界:
- 第一境界:能写出基础递归,但性能堪忧
- 第二境界:运用闭包实现记忆化,大幅提升性能
- 第三境界:掌握动态规划,实现最优解
这个过程不仅仅是算法的优化,更是思维方式的升级:
- 从问题导向到解决方案导向
- 从单一思路到多角度思考
- 从能实现功能到追求最优性能
在实际开发中,我们经常需要在代码可读性和执行效率之间做权衡。记忆化递归代码更直观,动态规划性能更优,具体选择需要根据实际场景来决定。
最后,送给大家一句话:算法不是为了刷题而刷题,而是为了培养解决问题的思维方式。掌握了这种思维,无论是面试还是实际开发,都能游刃有余!