链表递归遍历,核心是递归的"递"与"归" 对应链表遍历的"走下去"和"走回来",
一、先明确核心:递归遍历链表的本质
链表是"一串节点连起来"的结构(节点1 → 节点2 → 节点3 → null),递归遍历的逻辑和"你挨个喊小朋友名字"是一样的:
- 先喊当前小朋友(节点)的名字(前序);
- 让当前小朋友去喊下一个小朋友(递归调用
traverse(head.next)); - 等下一个小朋友喊完回来,再做后续事(后序);
- 直到喊到"没人了"(
head == null),就停止。
二、逐行拆解代码(最关键的一步)
java
void traverse(ListNode head) {
// 1. 递归终止条件:"递归的出口"
if (head == null) return;
// 2. 前序位置:"递"的过程中,第一次遇到当前节点时执行
// 比如:打印当前节点值、统计节点数、修改节点值等
traverse(head.next); // 3. 递归调用:去遍历下一个节点
// 4. 后序位置:"归"的过程中,遍历完下一个节点后回到当前节点时执行
// 比如:倒序打印节点、反转链表等
}
关键拆解:
-
终止条件
if (head == null) return这是递归的"刹车"!如果没有这行,递归会一直调用
traverse(head.next),直到head是null时继续访问null.next,直接抛出空指针异常。比如链表是
1→2→3→null,当遍历到3的下一个节点(null)时,触发终止条件,不再递归,开始"往回走"。 -
前序位置(递归调用前)
「递」的阶段------第一次碰到当前节点时执行。
比如链表
1→2→3,前序执行顺序是:先处理1→ 再处理2→ 再处理3(正序)。 -
traverse(head.next)让递归"往下走",告诉程序:"先把当前节点放一放,我先去遍历下一个节点,等下一个遍历完了再回来处理当前节点的后序逻辑"。
-
后序位置(递归调用后)
「归」的阶段------遍历完下一个节点,"回头"处理当前节点。
比如链表
1→2→3,后序执行顺序是:先处理3→ 再处理2→ 再处理1(倒序)。
三、用"打印节点"的例子,直观理解执行流程
我们给框架加一点逻辑,打印链表节点值,看前/后序的区别:
例子1:前序打印(正序)
java
void traverse(ListNode head) {
if (head == null) return;
// 前序位置:打印当前节点
System.out.println(head.val);
traverse(head.next);
}
// 链表 1→2→3,输出:1 2 3
执行流程(像"走下去"):
- 调用
traverse(1)→ 打印 1 → 调用traverse(2) - 调用
traverse(2)→ 打印 2 → 调用traverse(3) - 调用
traverse(3)→ 打印 3 → 调用traverse(null) traverse(null)触发终止,返回- 整个过程结束
例子2:后序打印(倒序)
java
void traverse(ListNode head) {
if (head == null) return;
traverse(head.next);
// 后序位置:打印当前节点
System.out.println(head.val);
}
// 链表 1→2→3,输出:3 2 1
执行流程(像"走下去再走回来"):
- 调用
traverse(1)→ 先调用traverse(2)(不打印1) - 调用
traverse(2)→ 先调用traverse(3)(不打印2) - 调用
traverse(3)→ 先调用traverse(null)(不打印3) traverse(null)触发终止,返回- 回到
traverse(3)→ 打印 3 → 返回 - 回到
traverse(2)→ 打印 2 → 返回 - 回到
traverse(1)→ 打印 1 → 返回 - 整个过程结束
四、为什么要这么写?(核心价值)
你可能会问:"用 for 循环遍历链表不就行了,为啥要递归?"
答:递归把"遍历逻辑"和"业务逻辑"分离,解决复杂链表问题时更简单 。
比如"反转链表",用递归后序位置写,逻辑极其清晰:
java
ListNode reverse(ListNode head) {
if (head == null || head.next == null) return head;
// 先反转下一个节点开始的链表
ListNode newHead = reverse(head.next);
// 后序位置:让下一个节点指向自己
head.next.next = head;
// 断开当前节点的next,避免环
head.next = null;
return newHead;
}
如果用迭代(for/while)写,需要记指针操作的细节;但用递归框架,只需要在"后序位置"填逻辑就行------这就是框架的意义。
五、特别说明
其他都好理解,但是可能你会有疑问。为什么当head等于null,已经return了,程序还能继续运行?为什么还会在执行调用递归那个方法后面的后序部分呢?
这是递归的核心精髓,关键结论先告诉你:
return 只结束当前这一层 的 traverse 方法,不是结束整个递归流程;只有当前层结束,才会回到上一层继续执行递归调用后面的代码。
我用「现实场景+代码执行细节」帮你彻底掰明白:
一、先举个生活例子(秒懂逻辑)
把递归想象成「你和3个朋友传话」:
- 你(第一层:traverse(1))对朋友A说:"你先去跟朋友B传话,等B传完回来,我再跟你说最后一句话";
- 朋友A(第二层:traverse(2))对朋友B说:"你先去跟朋友C传话,等C传完回来,我再跟你说最后一句话";
- 朋友B(第三层:traverse(3))对朋友C说:"你先去跟空座位传话,等空座位传完回来,我再跟你说最后一句话";
- 朋友C走到空座位(第四层:traverse(null)),发现没人,转身回来告诉B:"没人,我传完了"(触发return);
- 朋友B看到C回来,才说自己的"最后一句话"(打印3),然后告诉A:"我传完了";
- 朋友A看到B回来,说自己的"最后一句话"(打印2),然后告诉你:"我传完了";
- 你看到A回来,说自己的"最后一句话"(打印1),整个流程结束。
👉 这里的关键:
- 朋友C对空座位的"return",只代表「C的传话任务结束」,不影响B/A/你的任务;
- 只有C结束,B才能执行"等C回来后"的话;只有B结束,A才能执行;以此类推。
二、代码层面拆解(逐行跟踪)
还是以 traverse(3) 调用 traverse(null) 为例,我们把每一层的代码执行步骤标出来:
第一层:traverse(1)
java
void traverse(ListNode head) { // head=1
if (head == null) return; // 不触发,head≠null
traverse(head.next); // 调用traverse(2) → 暂停在这里,等traverse(2)结束
System.out.println(head.val); // 【待执行】等traverse(2)回来才执行
}
第二层:traverse(2)
java
void traverse(ListNode head) { // head=2
if (head == null) return; // 不触发
traverse(head.next); // 调用traverse(3) → 暂停,等traverse(3)结束
System.out.println(head.val); // 【待执行】
}
第三层:traverse(3)
java
void traverse(ListNode head) { // head=3
if (head == null) return; // 不触发
traverse(head.next); // 调用traverse(null) → 暂停,等traverse(null)结束
System.out.println(head.val); // 【待执行】
}
第四层:traverse(null)
java
void traverse(ListNode head) { // head=null
if (head == null) return; // 触发!执行return → 这一层方法结束
traverse(head.next); // 根本没机会执行
System.out.println(head.val); // 根本没机会执行
}
关键:第四层结束后,回到第三层
当 traverse(null) 执行完 return 后,程序会回到调用它的地方 ------也就是 traverse(3) 里的 traverse(head.next) 这一行!
此时 traverse(3) 中暂停的代码继续执行:
java
void traverse(ListNode head) { // head=3
if (head == null) return;
traverse(head.next); // 已经执行完(因为traverse(null)结束了)
System.out.println(head.val); // 现在执行!打印3 → 然后traverse(3)结束
}
接着:第三层结束,回到第二层
traverse(3) 结束后,回到 traverse(2) 的 traverse(head.next) 行,继续执行:
java
void traverse(ListNode head) { // head=2
if (head == null) return;
traverse(head.next); // 执行完(traverse(3)结束)
System.out.println(head.val); // 执行!打印2 → traverse(2)结束
}
最后:第二层结束,回到第一层
同理,traverse(2) 结束后,traverse(1) 执行打印1,整个递归流程结束。
三、核心结论(一句话记住)
return 只结束当前调用的这一层方法 ,递归调用的本质是「多层方法嵌套调用」:
- 每一层的
traverse都是一个独立的方法执行实例; - 只有"下一层"的
traverse执行完(不管是正常结束还是return结束),"上一层"才会继续执行递归调用后面的代码; - 终止条件的
return只是让"最底层"的方法结束,从而触发从下到上的"归"过程。
四、再用"调用栈"验证
调用栈的特点是「先进后出」:
- 递的阶段:traverse(1) → traverse(2) → traverse(3) → traverse(null)(依次压入栈);
- traverse(null)执行return → 弹出栈(栈里剩1、2、3);
- 归的阶段:traverse(3)继续执行后序代码 → 弹出 → traverse(2)继续 → 弹出 → traverse(1)继续 → 弹出。
总结
这段递归遍历代码的核心可以总结为 3 点:
- 终止条件是"刹车" :
if (head == null) return避免空指针,也是递归"往回走"的起点; - 前序=先做事,后序=回头做事:前序是"递"(往下走),后序是"归"(往回走);
- 框架复用性强:不管是打印、统计、反转链表,都能套这个框架,只改前/后序的业务逻辑。