一、基础概念 & 核心思想
1. 什么是递归
递归 :方法自身调用自身,把一个大问题 拆解为同类型的子问题 ,直到触达终止条件,再逐层回溯得到结果。通俗理解:自己调用自己,大事拆小事,小事解决再回头合并结果。
2. 递归两大必备要素(必背,缺一不可)
- 递归出口(终止条件) 问题最小单元,不再继续调用自身,防止无限递归。
- 递归体(递推公式 / 递推逻辑) 把原问题拆解为更小的同类型子问题,执行
方法(更小参数)。
3. 执行流程
分为两个阶段:
- 递(递推):从原始问题出发,不断调用自身,参数逐步缩小,直到触发出口。
- 归(回溯):从出口开始,逐层向上返回结果,合并得到最终答案。
二、基础语法 & 入门示例
示例 1:求 n 的阶乘(经典入门)
公式:n! = n * (n-1)!,出口:0! = 1、1! = 1
public class RecursionDemo {
// 递归方法
public static int factorial(int n) {
// 1. 递归出口
if (n == 0 || n == 1) {
return 1;
}
// 2. 递归体:拆解子问题
return n * factorial(n - 1);
}
public static void main(String[] args) {
System.out.println(factorial(5)); // 输出 120
}
}
执行流程(factorial(5)):factorial(5) → 5 * factorial(4)``factorial(4) → 4 * factorial(3)``factorial(3) → 3 * factorial(2)``factorial(2) → 2 * factorial(1)``factorial(1) → 触发出口,返回 1回溯计算 :21 → 3 2 → 46 →524 = 120
示例 2:斐波那契数列
规则:F(n) = F(n-1) + F(n-2),出口:F(1)=1,F(2)=1
public static int fib(int n) {
if (n == 1 || n == 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
三、递归核心分类 & 实战运用场景(项目常用)
根据执行逻辑分为直接递归 、间接递归;根据业务场景划分高频落地场景。
1. 递归分类
- 直接递归:方法 A 直接调用 A(上面阶乘、斐波那契都是)。
- 间接递归:A 调用 B,B 又调用 A,业务中极少使用,易死循环,不推荐。
2. 项目高频运用场景(重点背诵)
场景 1:树形结构遍历(最常用)
业务:部门树、菜单树、分类树、文件目录、权限树。特点:每个节点的子节点和父节点结构完全一致,天然适合递归。
// 节点实体
class Dept {
private Long id;
private String name;
private List<Dept> children;
// getter/setter
}
// 递归遍历整棵部门树
public static void traverseDept(Dept dept) {
// 处理当前节点
System.out.println(dept.getName());
// 递归遍历子节点
if (dept.getChildren() != null) {
for (Dept child : dept.getChildren()) {
traverseDept(child);
}
}
}
场景 2:文件 / 目录遍历
遍历本地文件夹下所有文件、子文件夹,文件层级不确定,递归最优。
public static void listFile(File file) {
if (file.isFile()) {
System.out.println("文件:" + file.getAbsolutePath());
return;
}
// 文件夹:遍历子项,递归
File[] files = file.listFiles();
if (files != null) {
for (File f : files) {
listFile(f);
}
}
}
场景 3:算法类问题
- 数列计算:阶乘、斐波那契、累加求和
- 回溯算法:全排列、组合、迷宫、八皇后(递归 + 回溯组合)
场景 4:层级数据计算
多级佣金、多级评论楼中楼、上下级关系统计。
四、递归核心关键点(学习 & 编码必记)
1. 优先确定「递归出口」
写递归第一步先写出口,再写递归体。
- 出口必须唯一 / 明确,覆盖所有终止情况;
- 出口位置建议写在方法最开头,代码可读性最高。
2. 保证参数向出口收敛
每次递归调用,传入的参数必须不断靠近终止条件 。例:factorial(n-1),n 持续变小,最终走到 n=1;如果写成 factorial(n+1),参数越来越大,永远触达不了出口 → 死递归。
3. 理解栈帧(JVM 底层执行)
每调用一次递归方法,JVM 都会在虚拟机栈 中创建一个栈帧:
- 保存局部变量、方法参数、返回地址;
- 递归深度 = 栈帧数量;
- 所有递归执行完毕后,栈帧逐层出栈,完成回溯。
4. 递归与循环的取舍
表格
| 维度 | 递归 | 循环 |
|---|---|---|
| 代码 | 简洁、逻辑贴近问题本身 | 代码偏繁琐 |
| 可读性 | 树形、层级问题可读性极高 | 复杂层级逻辑难写 |
| 性能 | 有栈帧开销,性能偏弱 | 无额外开销,性能高 |
| 深度限制 | 受栈深度限制 | 无栈深度限制 |
选型原则:
- 树形、层级、分支结构 → 优先递归;
- 简单重复计算、数据量大、层级极深 → 优先循环 / 迭代。
5. 递归 + 回溯 组合用法
很多场景需要递归遍历 + 状态回溯 (比如全排列、迷宫):核心逻辑:递(前进) → 处理逻辑 → 归(恢复状态)示例伪代码:
java
void dfs(参数){
if(出口) return;
// 1. 选择当前状态
标记状态
// 2. 递归向下
dfs(子参数);
// 3. 回溯:恢复状态(关键)
撤销标记
}
五、递归常见坑 & 解决方案(面试 + 线上故障高频)
坑 1:缺少递归出口 / 出口错误 → 无限递归 → 栈溢出 StackOverflowError
问题表现
程序运行直接抛出 java.lang.StackOverflowError,线程卡死。
原因
- 完全没写终止条件;
- 出口条件判断错误(如
n <= 0写成n < 0); - 参数不收敛,越递归离出口越远。
解决方案
- 编码先写出口,反复校验条件;
- 手动推演小数据,验证参数变化方向;
- 边界值单独测试(如 n=0、n=1)。
坑 2:递归深度过大 → 栈溢出 StackOverflowError
问题原因
JVM 虚拟机栈默认容量有限(默认栈深度一般 几千层 )。当递归层级上万 / 十万级,栈帧数量超出栈容量,直接溢出。典型场景:遍历超深目录、超长链表递归、大数据量数列计算。
解决方案
- 改递归为迭代(循环)(最优方案);
- 调大 JVM 栈参数
-Xss(治标不治本,不推荐线上使用); - 使用尾递归(Java 不支持尾递归优化,此方案作废)。
补充:Java 虚拟机没有实现尾递归优化,哪怕写成尾递归,深度过大依然栈溢出。
坑 3:重复计算 → 性能爆炸、耗时极长
典型案例:原生斐波那契数列
fib(5) 会重复计算 fib(3)、fib(2) 多次;n 越大,重复计算呈指数级增长。n=40 以上就能明显感觉到卡顿。
解决方案
- **记忆化递归(缓存中间结果)**用 Map / 数组 保存已经计算过的结果,避免重复递归:
java
// 缓存:key=参数n,value=计算结果
private static Map<Integer, Integer> cache = new HashMap<>();
public static int fibMem(int n) {
if (n == 1 || n == 2) {
return 1;
}
// 命中缓存直接返回
if (cache.containsKey(n)) {
return cache.get(n);
}
int res = fibMem(n - 1) + fibMem(n - 2);
cache.put(n, res); // 存入缓存
return res;
}
- 直接改为循环迭代。
坑 4:递归中共享全局变量 → 线程不安全、结果错乱
问题
递归方法使用静态变量、全局成员变量存储结果,多线程 / 多次调用时,变量值被覆盖,结果错误。
java
// 错误示例:全局变量累加
private static int sum = 0;
public static void sumNum(int n) {
if (n < 1) return;
sum += n;
sumNum(n - 1);
}
多次调用 sumNum 时,sum 不会重置,数据错乱。
解决方案
- 把结果作为方法返回值传递(推荐),不依赖全局变量;
- 如需中间存储,使用局部变量 或线程本地变量
ThreadLocal。
坑 5:递归回溯时状态未恢复 → 业务逻辑错误
多见于树形遍历、回溯算法(全排列、迷宫)。
问题
递归向下时修改了对象状态,回溯时没有还原,导致后续分支数据异常。
解决方案
严格遵循:修改状态 → 递归 → 恢复状态 的三步流程。
坑 6:间接递归(A 调 B、B 调 A)→ 逻辑复杂,易死循环
业务中尽量杜绝间接递归,可读性差、排障困难,优先重构为直接递归或循环。
六、递归优化方案总结(面试高频)
- 记忆化递归(备忘录):解决重复计算,用缓存存中间结果;
- 递归转迭代:解决栈溢出、深度过大问题,线上大数据首选;
- 拆分递归层级:大问题拆分为多段小递归,降低单次深度;
- 杜绝全局变量,使用返回值传递数据,保证线程安全。
七、面试高频简答 & 背诵总结
1. 递归的两大必要条件?
递归出口(终止条件) + 递归体(子问题拆解),且参数必须向出口收敛。
2. 递归为什么会出现 StackOverflowError?
每一次递归调用都会创建栈帧,JVM 虚拟机栈容量有限,递归深度超过栈最大容量,就会栈溢出。
3. 如何解决递归重复计算?
使用记忆化递归,通过数组 / Map 缓存已计算的结果,避免重复调用。
4. 递归和循环怎么选型?
树形、层级、分支复杂场景用递归(代码简洁);数据量大、层级深、性能要求高用循环。
5. Java 支持尾递归优化吗?
不支持,即使编写尾递归代码,深度过大依然会栈溢出。
八、终极背诵口诀
递归两要素:出口加递归体; 参数向收敛,栈帧一层层; 树形目录层级事,递归写来最省事; 深度太大栈溢出,改作迭代最靠谱; 重复计算加缓存,全局变量要杜绝; 回溯记得恢复态,线上避坑不出错。