一、递归:用 "拆礼物" 思维解决问题
(一)递归的核心逻辑:自顶向下的拆解
递归就像拆礼物 ------ 当你遇到一个大问题(比如求斐波那契数列第 n 项),先别慌!试着把它拆成两个更小的同类问题:f(n) = f(n-1) + f(n-2)
。这时候你会发现,每个小问题又能继续拆,直到遇到 "拆不动" 的基本情况(比如n≤1
时直接返回 n)。这种 "站在问题终点想解法" 的思路,就是递归的灵魂。
举个栗子🌰,经典斐波那契数列的递归实现:
javascript
function fib(n) {
if (n <= 1) return n; // 退出条件:拆到最小礼物就停手
return fib(n-1) + fib(n-2); // 拆成两个小礼物,结果拼起来
}
(二)递归的 "甜蜜烦恼":重复计算与栈溢出
不过递归有个小毛病 ------ 太 "实诚" 了!比如算fib(10)
时,fib(8)
会被算两次,fib(7)
会被算三次(画个树状图秒懂:f(10)
下面是f(9)
和f(8)
,f(9)
又拆出f(8)
和f(7)
......)。这种重复计算让时间复杂度飙升到O(2ⁿ)
,算fib(1000)
?计算机怕是要 "罢工" 咯!
还有个隐藏风险 ------ 调用栈溢出。每次调用函数都会压栈,递归深度太深(比如几万层),栈内存就爆了,程序直接崩溃,堪称 "递归刺客"。
二、闭包:给递归配个 "小本本" 做记忆
(一)闭包优化递归:记住算过的结果
别急,闭包来救场啦!闭包就像一个 "小本本",能把递归算过的结果存起来,下次用到直接查,避免重复计算。具体怎么做?看下面的 "记忆化" 操作:
javascript
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(); // 闭包生成的fib函数自带记忆功能
console.log(fib(100)); // 秒出结果,再也不怕重复计算啦~
这里的关键是利用闭包的 "变量捕获":外层函数的cache
被内层fib
记住,每次调用都能访问,形成一个私有缓存空间。这波操作把时间复杂度砍到O(n)
,效率飙升!
(二)闭包的适用场景:需要 "记住历史" 的递归
只要递归问题满足 "重复子问题"(比如斐波那契、爬楼梯),闭包就能派上用场。但注意哦,闭包会占用额外内存(存缓存),不过用空间换时间,在大数据量时血赚!
三、动态规划:从 "拆礼物" 到 "搭积木" 的逆袭
(一)动态规划的核心:自底向上搭积木
递归是 "从上往下拆",动态规划则是 "从下往上搭"。以爬楼梯问题为例:假设你要爬 n 阶楼梯,每次能走 1 或 2 步,求有多少种方法。
递归思路 (自顶向下):f(n) = f(n-1) + f(n-2)
,但重复计算严重(比如算f(4)
时,f(2)
会被算两次)。
动态规划思路(自底向上):先算小问题的解,存起来,再一步步算大问题。比如:
-
f(1) = 1
(直接走 1 步) -
f(2) = 2
(1+1 或直接 2 步) -
f(3) = f(2) + f(1) = 3
(最后一步是走 1 步到 3,或走 2 步到 3) -
...... 以此类推,用数组
dp
存每个f(i)
的结果。
代码实现:
javascript
function climbStairs(n) {
const dp = new Array(n + 1); // 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[i]
表示什么(比如这里是爬 i 阶的方法数)。 - 转移方程 :找到
dp[i]
和之前状态的关系(核心!比如dp[i] = dp[i-1] + dp[i-2]
)。 - 初始条件 :最小子问题的解(比如
i=1
和i=2
的情况)。
(三)动态规划的优化:空间压缩
如果发现dp[i]
只和前面几个状态有关(比如爬楼梯只和前两个状态有关),可以不用存整个数组,只用几个变量就能搞定,把空间复杂度从O(n)
降到O(1)
:
javascript
function climbStairs(n) {
if (n === 1) return 1;
let a = 1, b = 2; // a=f(1), b=f(2)
for (let i = 3; i <= n; i++) {
const c = a + b; // c=f(i)
a = b; // 左移一位,a变成f(i-1)
b = c; // b变成f(i)
}
return b;
}
这波操作就像 "滚动数组",省内存又高效,面试官看了直点头~
四、三者关系:从递归到动态规划的进化之路
特性 | 递归 | 闭包优化递归 | 动态规划 |
---|---|---|---|
思路 | 自顶向下拆解 | 自顶向下 + 记忆化 | 自底向上递推 |
重复计算 | 有(指数级耗时) | 无(缓存复用) | 无(按顺序计算) |
空间问题 | 可能栈溢出 | 缓存占用内存 | 可优化到常数空间 |
适用场景 | 小规模问题、树状结构 | 中等规模重复子问题 | 大规模最值 / 计数问题 |
简单说:
- 递归是 "暴力拆礼物",适合理解问题但效率低;
- 闭包是 "拆礼物 + 记笔记",优化重复计算;
- 动态规划是 "按顺序搭积木",适合大规模问题,是递归的 "终极进化形态"。
五、面试官爱考啥?记住这三个 "灵魂拷问"
(一)递归的缺点是什么?怎么优化?
答:缺点是重复计算和栈溢出。优化方法有两种:
- 用闭包或数组做记忆化(适合自顶向下递归);
- 改用动态规划自底向上计算(适合大规模问题)。
(二)动态规划的状态转移方程怎么找?
答:关键是找 "最后一步" 的决策。比如爬楼梯,最后一步要么是走 1 阶(前面是 n-1 阶的解),要么是走 2 阶(前面是 n-2 阶的解),所以dp[n] = dp[n-1] + dp[n-2]
。
(三)闭包在优化递归时起到什么作用?
答:闭包提供了一个私有缓存空间(比如cache
对象),让递归函数能记住之前算过的结果,避免重复计算。这其实是 "记忆化搜索" 的核心思想,在算法题中超级实用!
六、总结:算法三剑客,各有各的范儿
-
递归是 "思路担当",简单直接,适合小规模问题;
-
闭包是 "优化小能手",用记忆化让递归效率起飞;
-
动态规划是 "效率王者",自底向上解决大规模问题,是算法面试的常客。
下次遇到类似问题(比如斐波那契、爬楼梯、零钱兑换),记得按这个套路来:先用递归理清思路,再看有没有重复子问题,有的话用闭包优化,最后转成动态规划提升效率。搞定这一套,面试官都得夸你 "思路清晰,优化到位"!