链表加一问题中的「虚拟头节点」魔法
今天我们来拆解 LeetCode 369 题「给单链表加一」,重点分析代码中巧妙使用的 虚拟头节点(dummy node) 设计模式。这一设计在链表问题中非常常见,掌握它能让你在处理边界条件时游刃有余。
问题描述
给定一个单链表表示的非负整数(最高位在链表头),将该整数加一。例如:
- 输入
[1,2,3]
→ 输出[1,2,4]
- 输入
[9,9,9]
→ 输出[1,0,0,0]
核心思路分析
加一操作的关键在于处理进位:
- 找到最后一个非9的节点:从右到左遍历链表,找到最后一个不是9的节点(因为它加一后不会产生连续进位)。
- 处理进位:将该节点加一,后续所有节点置0。
- 处理最高位进位:如果原链表所有节点都是9(如
[9,9,9]
),加一后需要在链表最前面新增一个1。
为什么需要虚拟头节点?
当链表所有节点都是9时,加一后需要创建新的头节点。此时,虚拟头节点可以:
- 统一处理头节点变化的情况(无需额外判断是否需要新增节点)。
- 简化代码逻辑,避免边界条件的冗余判断。
代码逐行解析
public class Solution {
public ListNode PlusOne(ListNode head) {
if (head == null) return new ListNode(1); // 空链表特殊处理
ListNode dummy = new ListNode(0); // 创建虚拟头节点
dummy.next = head; // 虚拟头节点指向原链表头
ListNode notNine = dummy; // 记录最后一个非9节点
// 第一步:找到最后一个非9节点
ListNode current = head;
while (current != null) {
if (current.val != 9) {
notNine = current; // 更新最后一个非9节点
}
current = current.next;
}
// 第二步:最后一个非9节点加一
notNine.val++;
notNine = notNine.next; // 移动到后续节点
// 第三步:后续节点全部置0
while (notNine != null) {
notNine.val = 0;
notNine = notNine.next;
}
// 返回结果:如果虚拟头节点的值为0,说明未修改原头节点,否则返回虚拟头节点
return dummy.val == 0 ? head : dummy;
}
}
虚拟头节点的作用演示
假设原链表为 [9,9,9]
:
- 初始化:
dummy.next
指向原头节点9
。 - 寻找最后一个非9节点:遍历后发现所有节点都是9,因此
notNine
仍为dummy
(初始值为0)。 - 加一操作:
dummy.val++
→dummy
变为1。 - 后续节点置0:原链表所有节点变为0。
- 返回结果:
dummy.val != 0
,返回dummy
,最终链表为[1,0,0,0]
。
为什么不用虚拟头节点会更复杂?
如果不使用虚拟头节点,处理最高位进位时需要:
- 检查头节点是否为9。
- 如果所有节点都是9,创建新节点并返回。
- 否则返回原头节点。
这会导致代码中出现冗余的分支判断。而虚拟头节点将所有情况统一处理,代码更简洁。
总结
虚拟头节点的核心价值在于:
- 统一处理头节点变化:无论是新增节点还是修改原头节点,都通过同一逻辑处理。
- 简化边界条件:避免在代码中频繁判断头节点是否为空或是否需要修改。
这一设计模式在链表问题中广泛应用,例如:
- 反转链表
- 删除链表节点
- 合并多个有序链表
掌握虚拟头节点的使用,能让你在处理链表问题时更高效、优雅。