递归 VS 动态规划:从 “无限套娃计算器” 到 “积木式解题神器”

就占用你 5 分钟,让你搞懂递归和 DP 算法!

前言

你有没有试过:对着计算器按 "× 上一个数",按到手指酸?或者自己照镜子,镜子里面有个自己,镜子的镜子里面还有个自己?这就是递归 ------ 像台 "无限套娃计算器",算大数能把自己 "算死机";而动态规划是 "积木式解题神器",从最小块开始拼,再复杂的题都能稳准狠搞定。

一、递归:"套娃计算器" 的用法

递归就俩关键:找 "套娃公式"(规律)+ 找 "最小娃"(出口)

例 1:算阶乘 ------ 套娃停不下来?

如果要你计算5的阶乘,你会怎么做?大部分人第一想法应该都是:直接一个for循环不就完事了吗?没错:

javascript 复制代码
function mul(n) {
    let num = 1;
    for(let i = n; i >= 1; i--){
        num *= i;
    }
    return num;
}
console.log(mul(5));

但是你用你聪明的脑袋 想了又想,阶乘是 "5! = 5×4×3×2×1",套娃公式是 "n! = n × (n-1)!"(大娃里塞小娃);最小娃是 "1! = 1"(塞到最小的娃就停)。好像这样也能写出来!

看代码(递归版阶乘):

javascript 复制代码
function mul(n) {
    // 最小娃:1!直接返回 1
    if(n == 1){
        return 1;
    }
    // 套娃公式:大娃=自己×小娃
    return n * mul(n - 1);
}
console.log(mul(5));

我嘞个豆,答案✅️了,这就是我们今天的主角--大名鼎鼎的递归 。但这计算器有 bug:例如算mul(50000)会 "套娃太多死机"(栈溢出)。在我写的浅拷贝 VS 深拷贝这篇文章中有掘u问到了这个问题。所以递归一般在非必要情况下使用,因为数据一大就会爆栈。

例 2:斐波那契数列 ------ 套两层娃?

斐波那契是 "1,1,2,3,5......",套娃公式是 "第 n 项 = 第 n-1 项 + 第 n-2 项"(一个娃里塞俩小娃);最小娃是 "第 1、2 项 = 1"。

javascript 复制代码
function fb(n) {
    // 最小娃:前两个直接返回 1
    if(n == 1 || n == 2){
        return 1;
    } 
    // 套娃公式:自己=左边娃+右边娃
    return fb(n - 1) + fb(n - 2);
}
console.log(fb(5));

这计算器更费电:算fb(100)要重复掏同一批娃,卡到你怀疑人生~,这就是递归 ,好理解但难用,接下来我们开始经典算法--动态规划

二、动态规划:"积木神器" 怎么拼?

动态规划是 "反着来"------ 不用拆娃,直接从 最小积木块(已知结果) 开始,一块一块拼出最终答案,像搭乐高一样稳。

例:爬楼梯 - 力扣(LeetCode)

有一个思路,那就是直接暴力通项公式,看看官方题解:

javascript 复制代码
var climbStairs = function(n) {
    const sqrt5 = Math.sqrt(5);
    const fibn = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1);
    return Math.round(fibn / sqrt5);
};

但是这里我们用算法思想,也就是动态规划。积木公式:第 n 级的拼法 = 第 n-1 级拼法(最后加 1 块)+ 第 n-2 级拼法(最后加 2 块)。

看代码(动态规划版爬楼梯):

javascript 复制代码
var climbStairs = function (n) {
    // 积木盒dp:存每级台阶的拼法数
    let dp = [];
    // 基础积木:0级(起点)和1级各1种拼法
    dp[0] = 1;
    dp[1] = 1;
    // 从2级开始,用现有积木拼新台阶
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    // 第n级的拼法就是dp[n]
    return dp[n];
};
console.log(climbStairs(5)); 

这神器不卡不崩,再大的 n 都能轻松搞定~

总结:套娃计算器 vs 积木神器

  • 递归(套娃计算器):上手快,但算大数容易 "死机";
  • 动态规划(积木神器):从基础块开拼,稳、准、快,复杂题克星。

我最后总结这样一份表格,希望能够帮到各位:

维度 递归(Recursion) 动态规划(Dynamic Programming, DP)
核心思想 把大问题拆解为规模更小的子问题,通过调用自身解决子问题,最终合并子问题结果得到原问题解 将大问题拆解为重叠子问题,通过存储子问题的解(记忆化 / 表格)避免重复计算,自底向上或自顶向下求解
问题特征 1. 问题可自然拆解为独立子问题(无大量重复计算)2. 问题具有递归结构(如树形结构、分治场景)3. 子问题规模递减且无重叠 1. 问题具有重叠子问题 (子问题被重复求解)2. 问题具有最优子结构(原问题最优解由子问题最优解组成)3. 存在状态转移关系
适用场景 1. 分治算法(如归并排序、快速排序)2. 树形结构遍历 / 操作(如二叉树的遍历、求深度、路径和)3. 排列组合枚举(如全排列、子集生成)4. 回溯算法(如 N 皇后、迷宫问题)5. 问题子问题无重叠,递归深度可控 1. 优化类问题(如最短路径、最大收益、最小代价)2. 计数类问题(如不同路径数、解码方式数)3. 子问题大量重复的场景(如斐波那契数列、背包问题)4. 状态可清晰定义且存在转移关系的问题
实现方式 1. 纯递归(无记忆化,直接调用自身)2. 递归 + 记忆化(Top-Down DP,属于 DP 的一种) 1. 自顶向下(记忆化递归)2. 自底向上(迭代 + 表格,如二维 / 一维 DP 数组)
时间复杂度 纯递归:若存在重叠子问题,时间复杂度指数级(如斐波那契纯递归为 O (2ⁿ));无重叠子问题时为 O (n) 或 O (nlogn)(如归并排序) 消除重复计算,时间复杂度通常为 O (n)、O (nm) 等多项式级别(如斐波那契 DP 为 O (n),01 背包为 O (nm))
空间复杂度 纯递归:递归调用栈深度(如二叉树递归遍历为 O (h),h 为树高);若递归深度过大可能栈溢出 自底向上 DP:存储状态的数组 / 表格空间(如 O (n) 或 O (nm));可优化空间(如滚动数组),无栈溢出风险
代码风格 代码简洁、直观,符合问题的自然拆解逻辑,易编写和理解 需手动定义状态和转移方程,代码相对复杂,但效率更高
典型例子 1. 二叉树的前 / 中 / 后序遍历2. 归并排序 / 快速排序3. 全排列 / 组合总和4. 汉诺塔问题5. 回溯法解 N 皇后 1. 斐波那契数列(优化版)2. 01 背包 / 完全背包问题3. 最长公共子序列(LCS)4. 最短路径(Floyd-Warshall/Dijkstra 的 DP 思想)5. 最大子数组和(Kadane 算法)
局限性 1. 重叠子问题导致重复计算,效率低2. 递归深度过大易引发栈溢出(如 n=10000 的斐波那契递归)3. 函数调用开销 1. 需明确状态定义和转移方程,对问题建模要求高2. 不适用于子问题无重叠的场景(反而增加空间开销)
相关推荐
ohyeah5 小时前
使用 Vue 3 实现大模型流式输出:从零搭建一个简易对话 Demo
前端·vue.js·openai
GPTMirrors镜像系统5 小时前
JS 实现指定 UA 访问网站跳转弹窗提醒,解决夸克等浏览器兼容性问题
前端·javascript
踢球的打工仔5 小时前
前端html(2)
前端·算法·html
脾气有点小暴5 小时前
JavaScript 数据存储方法全解析:从基础到进阶
开发语言·javascript·ecmascript
CoderYanger5 小时前
动态规划算法-子数组、子串系列(数组中连续的一段):21.乘积最大子数组
开发语言·算法·leetcode·职场和发展·动态规划·1024程序员节
CoderYanger5 小时前
A.每日一题——3432. 统计元素和差值为偶数的分区方案
java·数据结构·算法·leetcode·1024程序员节
TL滕5 小时前
从0开始学算法——第八天(堆排序)
笔记·学习·算法·排序算法
Ayanami_Reii5 小时前
进阶数据结构-AC自动机
数据结构·算法·动态规划·字符串·ac自动机
报错小能手5 小时前
数据结构 AVL二叉平衡树
数据结构·算法