你真的懂递归吗?没那么复杂,但也没那么简单

作者:刘大华日期:2025/11/25

大家好,我是大华。 很多初学者都觉得简单的递归还可以看得懂,稍微复杂些的复杂就觉得很难,甚至有些工作几年的同事也对其避而远之。其实,只要掌握了正确的方法,递归并没有那么可怕!

一、什么是递归?

打个比方:想象一下,你站在一排长长的队伍里,你想知道你前面有几个人。 但你只能看到你前面那个人,看不到更前面的人。怎么办? 你问前面那个人:“兄弟,你前面有几个人?” 他也不知道,于是他又问更前面的人:“兄弟,你前面有几个人?” 就这样一直往前问…… 直到问到排在最前面的那个人,他说:“我前面没人,是0个。” 然后,这个答案开始往回传:

最前面的人说:“0个” 他后面的人说:“我前面有1个(就是他)” 再后面的人说:“我前面有2个”… 最后传到你这里:“你前面有 N 个”这个过程,就是递归!

递归的本质就是:把一个大问题,拆解成相同的小问题,直到遇到最简单的情况(边界),然后从最简单的情况开始,一层层把结果返回回去,最终解决大问题。

二、递归的两大核心要素

任何正确的递归函数,都必须包含两个关键部分:

1. 递归终止条件(Base Case)

这是递归的“刹车”,防止无限循环。

当问题小到不能再拆时,直接返回结果。

没有它,程序就会无限调用自己,最终导致栈的溢出(Stack Overflow)

2. 递归调用(Recursive Case)

函数调用自己,但传入的参数是更小规模的问题。

每次调用都在向终止条件靠近。

三、从经典例子开始:计算阶乘

先看最简单的阶乘:5! = 5 × 4 × 3 × 2 × 1

1/**
2 * 计算阶乘的递归函数
3 * @param {number} n - 要计算阶乘的数字
4 * @returns {number} - n的阶乘结果
5 */
6function factorial(n) {
7    // 1. 基准条件:0的阶乘是1,1的阶乘也是1
8    if (n === 0 || n === 1) {
9        console.log(`到达基准条件:factorial(${n}) = 1`);
10        return 1;
11    }
12    
13    // 2. 递归条件:n! = n × (n-1)!
14    // 3. 递归调用:问题规模从n变成n-1
15    console.log(`计算 factorial(${n}) = ${n} × factorial(${n - 1})`);
16    const result = n * factorial(n - 1);
17    console.log(`得到结果:factorial(${n}) = ${result}`);
18    
19    return result;
20}
21
22// 测试
23console.log("最终结果:5的阶乘 =", factorial(5));
24

运行结果:

1计算 factorial(5) = 5 × factorial(4)
2计算 factorial(4) = 4 × factorial(3)
3计算 factorial(3) = 3 × factorial(2)
4计算 factorial(2) = 2 × factorial(1)
5到达基准条件:factorial(1) = 1
6得到结果:factorial(2) = 2
7得到结果:factorial(3) = 6
8得到结果:factorial(4) = 24
9得到结果:factorial(5) = 120
10最终结果:5的阶乘 = 120
11

看到这个调用过程,是不是对递归有了直观感受?

四、理解递归的关键:调用栈

要真正理解递归,必须明白调用栈的概念。

调用栈就像叠汉堡:每次函数调用就加一片面包,函数返回就拿走一片。

1/**
2 * 演示递归调用栈
3 */
4function understandCallStack() {
5    function recursiveDemo(level, maxLevel) {
6        // 打印当前栈深度
7        const indent = "  ".repeat(level);
8        console.log(`${indent}进入第 ${level} `);
9        
10        // 基准条件:达到最大深度时停止
11        if (level >= maxLevel) {
12            console.log(`${indent} ${level} 层:到达基准条件,开始返回`);
13            return;
14        }
15        
16        // 递归调用
17        recursiveDemo(level + 1, maxLevel);
18        
19        console.log(`${indent}离开第 ${level} `);
20    }
21    
22    console.log("=== 递归调用栈演示 ===");
23    recursiveDemo(0, 3);
24}
25
26understandCallStack();
27

运行结果:

1=== 递归调用栈演示 ===
2进入第 0 
3  进入第 1 
4    进入第 2 
5      进入第 3 
6       3 层:到达基准条件,开始返回
7    离开第 2 
8  离开第 1 
9离开第 0 
10

这就是为什么递归深度太大会"栈溢出"——汉堡叠得太高,倒掉了!

五、实际应用:文件系统遍历

递归在实际开发中非常实用,比如遍历文件夹:

1/**
2 * 模拟文件系统结构
3 */
4const fileSystem = {
5    name: "根目录",
6    type: "folder",
7    children: [
8        {
9            name: "文档",
10            type: "folder",
11            children: [
12                { name: "简历.pdf", type: "file" },
13                { name: "报告.docx", type: "file" }
14            ]
15        },
16        {
17            name: "图片", 
18            type: "folder",
19            children: [
20                { 
21                    name: "旅行照片", 
22                    type: "folder", 
23                    children: [
24                        { name: "海滩.jpg", type: "file" }
25                    ]
26                },
27                { name: "头像.png", type: "file" }
28            ]
29        },
30        { name: "README.txt", type: "file" }
31    ]
32};
33
34/**
35 * 递归遍历文件系统
36 * @param {object} node - 当前节点
37 * @param {string} indent - 缩进字符串
38 */
39function traverseFileSystem(node, indent = "") {
40    // 基准条件:空节点直接返回
41    if (!node) return;
42    
43    // 打印当前节点
44    const icon = node.type === 'folder' ? '📁' : '📄';
45    console.log(`${indent}${icon} ${node.name}`);
46    
47    // 递归条件:如果是文件夹且有子节点,递归遍历
48    if (node.type === 'folder' && node.children) {
49        node.children.forEach(child => {
50            traverseFileSystem(child, indent + "  ");
51        });
52    }
53}
54
55console.log("=== 文件系统遍历 ===");
56traverseFileSystem(fileSystem);
57

运行结果:

1=== 文件系统遍历 ===
2📁 根目录
3  📁 文档
4    📄 简历.pdf
5    📄 报告.docx
6  📁 图片
7    📁 旅行照片
8      📄 海滩.jpg
9    📄 头像.png
10  📄 README.txt
11

六、递归的适用场景

1. 树形结构操作

  • 文件系统遍历
  • DOM树操作
  • 组织架构图
  • 菜单导航

2. 数学问题

  • 阶乘计算
  • 斐波那契数列
  • 汉诺塔问题

3. 分治算法

  • 归并排序
  • 快速排序

4. 回溯算法

  • 迷宫求解
  • 数独解题

七、递归的优缺点

优点:

  • 代码简洁:复杂问题简单化
  • 思路清晰:符合人类思维方式
  • 数学表达直接:数学公式容易转换

缺点:

  • 性能开销:函数调用有成本
  • 栈溢出风险:递归太深会崩溃
  • 调试困难:调用链长难跟踪

八、重要改进:避免重复计算

我们来看斐波那契数列的例子,并解决性能问题:

1/**
2 * 斐波那契数列:0, 1, 1, 2, 3, 5, 8, 13...
3 * 规律:每个数是前两个数之和
4 */
5
6// 原始版本:性能很差,有大量重复计算
7function fibonacciSlow(n) {
8    if (n === 0) return 0;
9    if (n === 1) return 1;
10    return fibonacciSlow(n - 1) + fibonacciSlow(n - 2);
11}
12
13// 优化版本:使用备忘录避免重复计算
14function fibonacciMemo(n, memo = {}) {
15    if (n in memo) return memo[n];
16    if (n === 0) return 0;
17    if (n === 1) return 1;
18    
19    memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
20    return memo[n];
21}
22
23// 迭代版本:性能最好,不会栈溢出
24function fibonacciIterative(n) {
25    if (n === 0) return 0;
26    if (n === 1) return 1;
27    
28    let prev = 0;
29    let curr = 1;
30    
31    for (let i = 2; i <= n; i++) {
32        const next = prev + curr;
33        prev = curr;
34        curr = next;
35    }
36    
37    return curr;
38}
39
40// 性能测试
41console.log("斐波那契数列第10项:");
42console.log("慢速版本:", fibonacciSlow(10));
43console.log("备忘录版本:", fibonacciMemo(10));
44console.log("迭代版本:", fibonacciIterative(10));
45

九、常见错误和解决方案

错误1:忘记基准条件

1// 错误:无限递归!
2function infiniteRecursion(n) {
3    return n * infiniteRecursion(n - 1); // 没有停止条件!
4}
5
6// 正确:必须有基准条件
7function correctRecursion(n) {
8    if (n <= 1) return 1; // 基准条件
9    return n * correctRecursion(n - 1);
10}
11

错误2:问题规模没有减小

1// 错误:问题规模没有变小
2function wrongRecursion(n) {
3    if (n <= 1) return 1;
4    return n * wrongRecursion(n); // 还是n,没有减小!
5}
6
7//  正确:每次递归问题规模都要减小
8function correctRecursion(n) {
9    if (n <= 1) return 1;
10    return n * correctRecursion(n - 1); // n-1,问题规模减小
11}
12

十、调试技巧:

  1. 打印日志:跟踪递归过程
  2. 使用调试器:观察调用栈变化
  3. 先写基准条件:确保不会无限递归
  4. 小数据测试:先用小数据验证正确性

十一、什么时候该用递归?

适合用递归的情况:

  • 问题可以分解为相似的子问题
  • 数据结构本身是递归的(如树、图)
  • 解决方案需要回溯

不适合用递归的情况:

  • 性能要求极高
  • 递归深度可能很大
  • 可以用简单循环解决

十二、实际例子:计算数组深度

让我们用递归解决一个实际问题:

1/**
2 * 计算嵌套数组的深度
3 * 例如:[1, [2, [3, [4]]]] 的深度是4
4 */
5function calculateDepth(arr) {
6    // 基准条件:如果不是数组,深度为0
7    if (!Array.isArray(arr)) {
8        return 0;
9    }
10    
11    // 基准条件:空数组深度为1
12    if (arr.length === 0) {
13        return 1;
14    }
15    
16    // 递归条件:深度 = 1 + 子元素的最大深度
17    let maxChildDepth = 0;
18    for (const item of arr) {
19        const childDepth = calculateDepth(item);
20        if (childDepth > maxChildDepth) {
21            maxChildDepth = childDepth;
22        }
23    }
24    
25    return 1 + maxChildDepth;
26}
27
28// 测试
29const testArrays = [
30    [1, 2, 3],                   // 深度1
31    [1, [2, 3]],                 // 深度2  
32    [1, [2, [3, [4]]]],          // 深度4
33    []                           // 深度1
34];
35
36testArrays.forEach((arr, index) => {
37    console.log(`数组${index + 1}:`, JSON.stringify(arr));
38    console.log(`深度:`, calculateDepth(arr));
39    console.log("---");
40});
41

总结

递归的核心思想:把大问题分解成相似的小问题

三个关键点:

  1. 基准条件 - 知道什么时候停止
  2. 递归条件 - 知道如何分解问题
  3. 递归调用 - 自己调用自己

使用建议

  • 先确定基准条件
  • 确保每次递归问题规模都减小
  • 注意性能,必要时改用迭代
  • 复杂递归考虑使用备忘录优化

递归就像剥洋葱,一层一层往里剥,直到找到核心。掌握了这个方法,你就能优雅地解决很多复杂问题了!

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot+Vue3 整合 SSE 实现实时消息推送》

《这20条SQL优化方案,让你的数据库查询速度提升10倍》

《SpringBoot 动态菜单权限系统设计的企业级解决方案》

《Vue3 + ElementPlus 动态菜单实现:一套代码完美适配多角色权限系统》


你真的懂递归吗?没那么复杂,但也没那么简单》 是转载文章,点击查看原文


相关推荐


Android模拟器检测全面指南:从基础到高级策略
陆业聪2025/11/23

一、核心检测维度与方法 检测Android模拟器的核心思路是识别其与真实设备在硬件、系统属性和行为特征上的差异。以下是经过实践验证的有效方法。 1.1 检查系统构建属性 模拟器的android.os.Build类中的属性值通常包含特定标识符,这是最基础的检测方式。 public static boolean isProbablyEmulator() { String model = Build.MODEL.toLowerCase(); String manufacturer =


Redis(138) Redis的模块如何开发?
Victor3562025/11/22

Redis 模块开发是一种扩展 Redis 功能的强大方式。通过模块,开发者可以向 Redis 添加新的命令、数据类型、事件处理器等。以下是开发 Redis 模块的详细步骤,包括必要的代码示例。 1. 包含必要的头文件 首先,需要包含 Redis 模块 API 的头文件 redismodule.h。该头文件定义了开发模块所需的所有函数和宏。 #include "redismodule.h" 2. 实现模块命令 每个模块命令对应一个处理函数。这些函数需要遵循特定的签名,即返回 int 类型,并接


C++对象模型_第五章_C++函数语义学
Mr_WangAndy2025/11/20

本文介绍C++对象模型之函数语义学,揭露C++成员函数的神秘面纱,探究C++多态的底层原理,虚继承,类型转换原理。 文章目录 第5章 函数语义学5.1 普通成员函数调用方式5.2虚成员函数、静态成员函数调用方式5.2.1 虚成员函数调用方式5.2.2 静态成员函数调用方式 5.3虚函数地址转换---vcall引入5.4 静动态类型、绑定,多态实现5.4.1 静态类型和动态类型5.4.2 静态绑定和动态绑定5.4.3 继承的非虚函数坑5.4.4 虚函数的动态绑定5.4.5 重


Android多SDK合并为单个JAR包的完整指南
安卓蓝牙Vincent2025/11/19

痛点 多 SDK 分散:每个功能模块单独提供 JAR,用户需要逐一集成和管理 调用复杂:不同模块间存在依赖和包名冲突,用户在项目中使用不方便 升级维护困难:每次更新都要同步多个 JAR,容易出错 一、核心原理 1.1 最推荐的方案:源码合并 + 下层库作为“源码目录”加入 多 SDK 合并时,最终有效的构建环境只有顶层 SDK,因此最稳定的方式是: 源码合并(sourceSets) + 移除模块依赖 + 将下层 SDK 作为源码目录引入(而不是 module) Android St


Python 的内置函数 super
IMPYLH2025/11/17

Python 内建函数列表 > Python 的内置函数 super Python 的内置函数 super() 是一个非常重要的内置函数,主要用于在子类中调用父类(超类)的方法。这个函数在面向对象编程中扮演着关键角色,特别是在处理继承关系时。 基本用法 super() 最常见的用法是在子类的初始化方法中调用父类的初始化方法: class Parent: def __init__(self, name): self.name = name class Child(


Python 的内置函数 pow
IMPYLH2025/11/16

Python 内建函数列表 > Python 的内置函数 pow Python 的内置函数 pow() 是一个用于计算幂运算的强大工具。它有两种基本用法,可以计算数值的幂次方,也支持进行模运算。 基本语法 pow(base, exp) 参数说明 base:底数,可以是整数或浮点数exp:指数,可以是整数或浮点数 使用示例 基本幂运算: pow(2, 3) # 返回8 (2的3次方) pow(2.5, 2) # 返回6.25 (2.5的平方) 带模运算: pow(2,


🔥 “Solo Coding”的近期热度解析(截至 2025 年末)
LeonGao2025/11/15

🧠 一、概念回顾 Solo Coding 并不是新词,但在过去一年随着 AIGC 编程辅助工具(如 Copilot、Cursor、TabNine、ChatGPT Code Interpreter) 的普及,它被重新定义为: 一个人独立开发完整系统,但具备团队级效率。 这与传统意义的“独立开发者(Indie Developer)”不同,核心在于借助 AI 的合作力量,实现准团队式的个人生产力爆发。 📈 二、热度增长趋势 时间区间关键词趋势


Python 的内置函数 iter
IMPYLH2025/11/14

Python 内建函数列表 > Python 的内置函数 iter Python 的内置函数 iter() 用于创建一个迭代器对象,它可以将可迭代对象(如列表、元组、字典、集合等)转换为迭代器,从而支持逐个访问元素的操作。 基本语法 iter(iterable, sentinel) iterable:必需参数,表示要转换为迭代器的可迭代对象(如列表、字符串等)。sentinel:可选参数,用于指定迭代停止的条件值(主要用于自定义迭代行为)。 示例说明 基本用法(无 sentinel


python+uniapp基于微信小程序的垃圾分类信息系统
Q_Q5110082852025/11/13

目录 项目介绍本项目具体实现截图开发技术大数据类设计开发的基本流程是:论文大纲结论源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式! 项目介绍 本文介绍了一款基于微信小程序的垃圾分类信息系统。该系统旨在帮助用户更便捷地了解垃圾分类知识,提高垃圾分类的准确性和效率。通过微信小程序平台,用户可以随时随地查询各类垃圾的归属类别,并获取详细的分类指导。 本研究首先进行了用户需求分析,明确了平台应具备的功能和特点。然后,利用微信小程序开发技术,设计并实现了该平台。课题主要分为


HTML 的 <svg> 标签
hubenchang05152025/11/11

#HTML 的 <svg> 标签 请查看 HTML 元素帮助手册 了解更多 HTML 元素。 如果 svg 不是根元素,svg 元素可以用于在当前文档(比如说,一个 HTML 文档)内嵌套一个独立的 svg 片段。这个独立片段拥有独立的视口和坐标系统。 #属性 请查看 HTML 元素的全局属性 了解 HTML 元素的全局属性。 #示例 <svg width="300" height="300" viewBox="0 0 300 300" xmlns="http://www.w3.org/

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2025 聚合阅读