从递归到动态规划:手把手教你玩转算法三剑客

一、递归:用 "拆礼物" 思维解决问题

(一)递归的核心逻辑:自顶向下的拆解

递归就像拆礼物 ------ 当你遇到一个大问题(比如求斐波那契数列第 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];
}

(二)动态规划的三大要素:状态、转移方程、初始条件

  1. 状态定义 :明确dp[i]表示什么(比如这里是爬 i 阶的方法数)。
  2. 转移方程 :找到dp[i]和之前状态的关系(核心!比如dp[i] = dp[i-1] + dp[i-2])。
  3. 初始条件 :最小子问题的解(比如i=1i=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. 用闭包或数组做记忆化(适合自顶向下递归);
  2. 改用动态规划自底向上计算(适合大规模问题)。

(二)动态规划的状态转移方程怎么找?

答:关键是找 "最后一步" 的决策。比如爬楼梯,最后一步要么是走 1 阶(前面是 n-1 阶的解),要么是走 2 阶(前面是 n-2 阶的解),所以dp[n] = dp[n-1] + dp[n-2]

(三)闭包在优化递归时起到什么作用?

答:闭包提供了一个私有缓存空间(比如cache对象),让递归函数能记住之前算过的结果,避免重复计算。这其实是 "记忆化搜索" 的核心思想,在算法题中超级实用!

六、总结:算法三剑客,各有各的范儿

  • 递归是 "思路担当",简单直接,适合小规模问题;

  • 闭包是 "优化小能手",用记忆化让递归效率起飞;

  • 动态规划是 "效率王者",自底向上解决大规模问题,是算法面试的常客。

下次遇到类似问题(比如斐波那契、爬楼梯、零钱兑换),记得按这个套路来:先用递归理清思路,再看有没有重复子问题,有的话用闭包优化,最后转成动态规划提升效率。搞定这一套,面试官都得夸你 "思路清晰,优化到位"!

相关推荐
FirstFrost --sy37 分钟前
数据结构之二叉树
c语言·数据结构·c++·算法·链表·深度优先·广度优先
森焱森1 小时前
垂起固定翼无人机介绍
c语言·单片机·算法·架构·无人机
搂鱼1145141 小时前
(倍增)洛谷 P1613 跑路/P4155 国旗计划
算法
Yingye Zhu(HPXXZYY)1 小时前
Codeforces 2021 C Those Who Are With Us
数据结构·c++·算法
ohMyGod_1231 小时前
React16,17,18,19新特性更新对比
前端·javascript·react.js
@大迁世界1 小时前
第1章 React组件开发基础
前端·javascript·react.js·前端框架·ecmascript
Hilaku1 小时前
从一个实战项目,看懂 `new DataTransfer()` 的三大妙用
前端·javascript·jquery
爱分享的程序员2 小时前
前端面试专栏-算法篇:20. 贪心算法与动态规划入门
前端·javascript·node.js
我想说一句2 小时前
事件委托与合成事件:前端性能优化的"偷懒"艺术
前端·javascript
爱泡脚的鸡腿2 小时前
Web第二次笔记
前端·javascript