在算法的世界里,递归、动态规划和闭包是非常重要的概念。它们在解决各种复杂问题时发挥着关键作用。今天,我们就结合具体的代码示例,深入探讨这些概念在 JavaScript 中的应用。🎉
一、递归基础:斐波那契数列的 “朴素解法” 🌱
1.1 递归的核心思想
递归本质是 “自顶向下” 的解题思路:将大问题拆解为相似的小问题,直到触达退出条件。以斐波那契数列为例,求解 f(n) 需先得到 f(n-1) 和 f(n-2),以此类推直到 f(0) 或 f(1)。
1.2 代码实现与问题分析
// 斐波那契数列递归实现
function fib(n) {
if (n <= 1) return n; // 退出条件:f(0)=0,f(1)=1
return fib(n - 1) + fib(n - 2);
}
console.log(fib(10)); // 输出:55
存在的问题:
-
重复计算:以
f(10)为例,其计算树中f(8)被计算 2 次,f(7)被计算 3 次,底层节点重复次数呈指数级增长(树形结构如下):f(10) f(9) f(8) f(8) f(7) f(7) f(6) ...(底层节点重复计算严重) -
时间复杂度:O (2ⁿ),随
n增大性能急剧下降。 -
爆栈风险:函数调用需入栈,递归深度过大会导致调用栈溢出(如
fib(1000)可能直接报错)。
二、闭包优化:用 “记忆化” 解决重复计算 🧠
2.1 闭包的记忆化原理
闭包的核心是函数嵌套 + 自由变量:外层函数定义存储容器(如 cache),内层函数通过作用域链访问该容器,实现计算结果的缓存(记忆化)。
2.2 优化后的斐波那契实现
// 闭包+记忆化优化斐波那契
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 fib = memoizedFib();
console.log(fib(10)); // 输出:55
优化效果:
- 时间复杂度:降至 O (n),每个值仅计算一次。
- 空间复杂度:O (n),需额外存储缓存数据。
三、爬楼梯问题:递归的 “进阶试炼” 🧗♂️
3.1 问题定义
“爬楼梯” 与斐波那契数列同属重叠子问题:每次可爬 1 或 2 阶,求到达第 n 阶的总方法数。核心逻辑:climbStairs(n) = climbStairs(n-1) + climbStairs(n-2)(最后一步要么从 n-1 阶爬 1 阶,要么从 n-2 阶爬 2 阶)。
3.2 三种解法对比
解法 1:纯递归(存在严重缺陷)
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)); // 报错:栈溢出(Maximum call stack size exceeded)
- 问题:当
n=100时,递归深度过大导致栈溢出,且重复计算量惊人(时间复杂度 O (2ⁿ))。
解法 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缓存结果,避免重复计算,时间复杂度降至 O (n),且解决了n=100时的计算问题。 - 不足:依赖全局变量,可能造成命名污染。
解法 3:动态规划(迭代实现)
// 自底向上的动态规划
const climbStairs = function(n) {
const dp = []; // dp[i] 表示到达第i阶的方法数
dp[1] = 1;
dp[2] = 2;
// 从3阶开始迭代计算,直到n阶
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // 状态转移方程
}
return dp[n];
};
-
核心思想:从已知结果(
dp[1]、dp[2])出发,自底向上推导未知结果,无需递归调用栈。 -
优势:
- 时间复杂度 O (n),无重复计算;
- 空间复杂度 O (n),可进一步优化至 O (1)(只需保存前两个值);
- 彻底避免栈溢出问题。
四、核心知识点总结 📚
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 纯递归 | O(2ⁿ) | O(n) | 小数据量、逻辑验证 | 易栈溢出、重复计算严重 |
| 递归 + 记忆化 | O(n) | O(n) | 中等数据量、需保留递归逻辑 | 需额外缓存空间 |
| 动态规划(迭代) | O(n) | O(n) | 大数据量、性能要求高 | 无需递归栈,适合大规模计算 |
关键结论:
- 递归是思路,动态规划是优化:当递归存在大量重复计算时,优先用动态规划的 “自底向上” 迭代法。
- 闭包的记忆化作用:通过自由变量缓存结果,是递归优化的 “轻量方案”,适合不想改写为迭代的场景。
- 栈溢出的本质:递归深度超过 JavaScript 引擎的调用栈限制(通常约 1 万层),迭代法则无此问题。
五、总结🎉
递归、动态规划和闭包在算法中都有各自的优势和适用场景。递归简洁直观,但可能存在重复计算和栈溢出问题;动态规划通过保存子问题的解避免了重复计算,适合解决具有重叠子问题和最优子结构的问题;闭包可以用于优化递归,实现记忆化功能。在实际开发中,我们需要根据具体问题选择合适的方法。希望通过今天的分享,大家对这些概念有了更深入的理解。😘