递归详解与应用

递归详解与应用

概述

递归 是一种编程技巧,函数调用自身来解决问题。递归的核心思想是:将大问题分解为相同的小问题,直到问题足够小可以直接求解

💡 提示: 核心思想
递归 = 自我调用 + 终止条件 + 问题分解


一、递归的三个要素

1.1 递归的本质

递归就是函数调用自己,但每次调用都在处理一个更小的问题。

1
2
3
4
5
6
7
8
9
10
11
大问题

分解为更小的问题 + 调用自己

继续分解

直到问题足够小

直接求解(返回结果)

逐层返回结果

1.2 递归的三个必要条件

条件 说明 重要性
递归关系 函数调用自身 必须
终止条件 何时停止递归 必须(没有会无限循环)
问题分解 如何将大问题分解为小问题 必须

1.3 递归的通用模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 递归函数的标准模板
返回值类型 recursiveFunction(参数) {
// 1. 终止条件(基础情况)
if (终止条件) {
return 基础情况的结果;
}

// 2. 递归关系(递归情况)
// 调用自身处理更小的问题
返回值 = recursiveFunction(更小的参数);

// 3. 处理结果
return 处理后的结果;
}

二、递归的执行过程

2.1 递归栈的概念

当函数调用自己时,每次调用都会在上创建一个新的函数执行环境。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
递归调用过程:
recursion(5)
↓ 调用
recursion(4)
↓ 调用
recursion(3)
↓ 调用
recursion(2)
↓ 调用
recursion(1)
↓ 到达终止条件
return 1
↑ 返回
return 2 * 1 = 2
↑ 返回
return 3 * 2 = 6
↑ 返回
return 4 * 6 = 24
↑ 返回
return 5 * 24 = 120

2.2 递归栈的内存占用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
栈内存:
┌─────────────────┐
│ recursion(1) │ ← 最后入栈
│ 局部变量: n=1 │
├─────────────────┤
│ recursion(2) │
│ 局部变量: n=2 │
├─────────────────┤
│ recursion(3) │
│ 局部变量: n=3 │
├─────────────────┤
│ recursion(4) │
│ 局部变量: n=4 │
├─────────────────┤
│ recursion(5) │ ← 最先入栈
│ 局部变量: n=5 │
└─────────────────┘

递归深度 = 5
栈空间 = 5 × 每个函数的空间

关键点

  • 每次递归调用都会占用栈空间
  • 递归深度越深,占用的栈空间越多
  • 栈空间有限,过深的递归会导致栈溢出

三、递归的实际例子

3.1 例子1:计算阶乘

问题分析

1
2
3
4
5
5! = 5 × 4!
4! = 4 × 3!
3! = 3 × 2!
2! = 2 × 1!
1! = 1(基础情况)

递归关系n! = n × (n-1)!
终止条件n == 1

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 递归方式
int factorial(int n) {
// 1. 终止条件
if (n == 1) {
return 1; // 基础情况
}

// 2. 递归关系
// 调用自身处理更小的问题
int result = n * factorial(n - 1);

// 3. 返回结果
return result;
}

// 调用
int result = factorial(5); // 返回 120

执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
factorial(5)
5 != 1,继续递归
↓ 计算 5 * factorial(4)
factorial(4)
4 != 1,继续递归
↓ 计算 4 * factorial(3)
factorial(3)
3 != 1,继续递归
↓ 计算 3 * factorial(2)
factorial(2)
2 != 1,继续递归
↓ 计算 2 * factorial(1)
factorial(1)
1 == 1,到达终止条件
return 1
return 2 * 1 = 2
return 3 * 2 = 6
return 4 * 6 = 24
return 5 * 24 = 120

时间复杂度:O(n)
空间复杂度:O(n)(递归栈深度)

3.2 例子2:链表遍历(树遍历的基础)

问题分析

1
2
3
4
5
6
链表:1 → 2 → 3 → null

遍历思路:
- 访问当前节点
- 递归遍历下一个节点
- 终止条件:节点为 null

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class ListNode {
int val;
ListNode next;

ListNode(int val) {
this.val = val;
}
}

// 递归遍历链表
void traverse(ListNode node) {
// 1. 终止条件
if (node == null) {
return; // 到达链表末尾
}

// 2. 处理当前节点
System.out.println(node.val);

// 3. 递归处理下一个节点
traverse(node.next);
}

// 调用
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);

traverse(head); // 输出:1 2 3

执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
traverse(node1)  // node1.val = 1
node1 != null
↓ 打印 1
↓ 调用 traverse(node2)
traverse(node2) // node2.val = 2
node2 != null
↓ 打印 2
↓ 调用 traverse(node3)
traverse(node3) // node3.val = 3
node3 != null
↓ 打印 3
↓ 调用 traverse(null)
traverse(null)
null == null,到达终止条件
return
return
return
return

关键点

  • 每次递归都处理一个节点
  • 通过 node.next 将问题分解为更小的问题
  • node == null 时停止递归

3.3 例子3:二叉树前序遍历

问题分析

1
2
3
4
5
6
7
8
9
10
11
12
树结构:
1
/ \
2 3

前序遍历:根 → 左子树 → 右子树
输出:1 2 3

递归思路:
1. 访问根节点
2. 递归遍历左子树
3. 递归遍历右子树

递归实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode(int val) {
this.val = val;
}
}

// 前序遍历
void preorder(TreeNode node) {
// 1. 终止条件
if (node == null) {
return;
}

// 2. 处理根节点
System.out.println(node.val);

// 3. 递归处理左子树
preorder(node.left);

// 4. 递归处理右子树
preorder(node.right);
}

// 调用
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);

preorder(root); // 输出:1 2 3

执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
preorder(node1)  // node1.val = 1
↓ node1 != null
↓ 打印 1
↓ 调用 preorder(node2) // 处理左子树
↓ preorder(node2) // node2.val = 2
↓ node2 != null
↓ 打印 2
↓ 调用 preorder(null) // 左子树为 null
return
↓ 调用 preorder(null) // 右子树为 null
return
return
↓ 调用 preorder(node3) // 处理右子树
↓ preorder(node3) // node3.val = 3
↓ node3 != null
↓ 打印 3
↓ 调用 preorder(null) // 左子树为 null
return
↓ 调用 preorder(null) // 右子树为 null
return
return
return

关键点

  • 递归处理三个部分:根、左子树、右子树
  • 每个递归调用都是独立的,处理一个子树
  • 通过 node == null 判断是否继续递归

四、递归的三个关键概念

4.1 递归的”信任”

关键思想:不要试图追踪整个递归过程,而是相信递归函数能正确处理更小的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 错误的思维方式
// 试图追踪整个递归过程
void preorder(TreeNode node) {
if (node == null) return;
System.out.println(node.val);
preorder(node.left); // 我需要知道这里发生了什么...
preorder(node.right); // 还有这里...
}

// ✅ 正确的思维方式
// 相信递归函数能正确处理子树
void preorder(TreeNode node) {
if (node == null) return;

// 1. 处理当前节点
System.out.println(node.val);

// 2. 相信 preorder 能正确处理左子树
preorder(node.left);

// 3. 相信 preorder 能正确处理右子树
preorder(node.right);
}

关键

  • 不要试图在脑子里模拟整个递归过程
  • 只需要确保:终止条件正确 + 递归关系正确
  • 相信递归函数能处理更小的问题

4.2 递归的”分治”

分治思想:将大问题分解为相同的小问题。

1
2
3
4
5
6
7
8
大问题:遍历整个树
↓ 分解
小问题1:处理根节点
小问题2:遍历左子树(与大问题相同,但规模更小)
小问题3:遍历右子树(与大问题相同,但规模更小)
↓ 递归处理小问题
↓ 合并结果
完成

关键

  • 每次递归都在处理一个更小的问题
  • 问题的结构相同,只是规模不同
  • 最终会到达基础情况(终止条件)

4.3 递归的”回溯”

回溯思想:递归会自动回溯,逐层返回结果。

1
2
3
4
5
递归下降:
level 1 → level 2 → level 3 → ... → 基础情况

回溯上升:
level 1 ← level 2 ← level 3 ← ... ← 返回结果
1
2
3
4
5
6
7
8
9
10
11
12
// 后序遍历(回溯的典型应用)
void postorder(TreeNode node) {
if (node == null) return;

postorder(node.left); // 先处理左子树
postorder(node.right); // 再处理右子树
System.out.println(node.val); // 最后处理根节点(回溯时)
}

// 执行过程:
// 1. 递归下降到叶子节点
// 2. 回溯时处理节点(后序)

五、递归 vs 迭代

5.1 对比表

维度 递归 迭代
代码简洁性 简洁、易读 复杂、需要手动管理状态
空间复杂度 O(h)(递归栈) O(1)(通常)
时间复杂度 相同 相同
栈溢出风险 高(深度大时)
理解难度 需要理解递归思想 相对直观
性能 略低(函数调用开销) 略高

5.2 递归 vs 迭代:链表遍历

递归方式

1
2
3
4
5
6
7
8
void traverse(ListNode node) {
if (node == null) return;
System.out.println(node.val);
traverse(node.next);
}

// 优点:简洁、易读
// 缺点:占用栈空间,链表很长时可能栈溢出

迭代方式

1
2
3
4
5
6
7
8
9
10
void traverse(ListNode node) {
ListNode current = node;
while (current != null) {
System.out.println(current.val);
current = current.next;
}
}

// 优点:不占用额外栈空间,效率高
// 缺点:需要手动管理指针

5.3 递归 vs 迭代:树遍历

递归方式(前序)

1
2
3
4
5
6
7
8
void preorder(TreeNode node) {
if (node == null) return;
System.out.println(node.val);
preorder(node.left);
preorder(node.right);
}

// 代码简洁,逻辑清晰

迭代方式(前序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void preorderIterative(TreeNode root) {
if (root == null) return;

Stack<TreeNode> stack = new Stack<>();
stack.push(root);

while (!stack.isEmpty()) {
TreeNode node = stack.pop();
System.out.println(node.val);

if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}

// 需要手动管理栈,逻辑复杂

关键观察

  • 递归本质上是利用系统栈
  • 迭代是手动管理栈
  • 递归简洁,迭代高效

六、递归的常见陷阱

6.1 陷阱1:没有终止条件

1
2
3
4
5
6
7
8
9
10
11
12
// ❌ 错误:无限递归
void recursion(int n) {
System.out.println(n);
recursion(n + 1); // 永远不会停止
}

// ✅ 正确:有终止条件
void recursion(int n) {
if (n > 10) return; // 终止条件
System.out.println(n);
recursion(n + 1);
}

6.2 陷阱2:终止条件永远不会到达

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:终止条件错误
void recursion(int n) {
if (n == 0) return;
System.out.println(n);
recursion(n - 2); // 如果 n 是奇数,永远不会等于 0
}

// ✅ 正确:终止条件正确
void recursion(int n) {
if (n <= 0) return; // 包含所有停止情况
System.out.println(n);
recursion(n - 2);
}

6.3 陷阱3:问题没有真正分解

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ 错误:问题没有变小
void recursion(int n) {
if (n == 0) return;
System.out.println(n);
recursion(n); // 问题大小没变,无限递归
}

// ✅ 正确:问题逐步变小
void recursion(int n) {
if (n == 0) return;
System.out.println(n);
recursion(n - 1); // 问题逐步变小
}

6.4 陷阱4:栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 危险:递归深度太大
void recursion(int n) {
if (n == 0) return;
recursion(n - 1);
}

recursion(100000); // 栈溢出!

// ✅ 解决:使用迭代或增加栈大小
// 方式1:使用迭代
for (int i = 100000; i > 0; i--) {
// 处理
}

// 方式2:增加栈大小(JVM 参数)
// java -Xss10m MyProgram

七、递归的应用场景

7.1 适合用递归的场景

场景 原因 例子
树/图遍历 天然的递归结构 前序、中序、后序遍历
分治算法 问题可分解 快速排序、归并排序
回溯算法 需要尝试所有可能 N 皇后、组合问题
动态规划 有重叠子问题 斐波那契数列

7.2 不适合用递归的场景

场景 原因 替代方案
线性遍历 迭代更高效 使用 while 循环
深度很大 容易栈溢出 使用迭代或增加栈大小
性能关键 函数调用开销大 使用迭代

八、递归的调试技巧

8.1 打印递归栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void preorder(TreeNode node, int depth) {
if (node == null) return;

// 打印缩进表示深度
String indent = " ".repeat(depth);
System.out.println(indent + "进入: " + node.val);

preorder(node.left, depth + 1);
preorder(node.right, depth + 1);

System.out.println(indent + "返回: " + node.val);
}

// 输出:
// 进入: 1
// 进入: 2
// 返回: 2
// 进入: 3
// 返回: 3
// 返回: 1

8.2 验证终止条件

1
2
3
4
5
6
7
8
9
10
11
void recursion(int n) {
// 添加日志验证终止条件
System.out.println("递归调用: n = " + n);

if (n == 0) {
System.out.println("到达终止条件");
return;
}

recursion(n - 1);
}

8.3 检查问题分解

1
2
3
4
5
6
7
8
9
10
11
void recursion(int n) {
System.out.println("处理问题大小: " + n);

if (n <= 1) return;

// 验证问题确实在变小
int nextSize = n - 1;
System.out.println("下一个问题大小: " + nextSize);

recursion(nextSize);
}

九、递归思维的培养

9.1 三步法

第1步:明确递归的三个要素

  • 终止条件是什么?
  • 递归关系是什么?
  • 如何分解问题?

第2步:相信递归

  • 不要试图追踪整个过程
  • 只需确保逻辑正确
  • 相信递归能处理更小的问题

第3步:验证

  • 测试基础情况
  • 测试简单情况(n=1, 2, 3)
  • 测试复杂情况

9.2 练习建议

1
2
3
4
5
6
7
8
// 从简单到复杂
1. 阶乘:factorial(n)
2. 斐波那契:fib(n)
3. 链表遍历:traverse(node)
4. 树遍历:preorder(node)
5. 二分查找:binarySearch(arr, target)
6. 快速排序:quickSort(arr)
7. 回溯:permutation(arr)

相关笔记

  • 属性数据结构遍历详解
  • 链表操作详解
  • 树的操作详解

递归详解与应用
https://zmmmmy.github.io/2026/01/12/递归详解与应用/
作者
ZhiMy
发布于
2026年1月12日
许可协议