链表题里为什么常用虚拟头节点 dummy?
简化版
虚拟头节点 dummy 是放在真实头节点前面的辅助节点,用来统一处理“头节点可能被删除、插入、替换”的边界。它不属于最终链表,最后返回 dummy.next,能让代码少写很多 head == null 或“删除第一个节点”的特殊分支。
详细版
链表题最麻烦的是头节点会变。例如删除值为 1 的节点,链表 [1,2,3] 删除后新头变成 2;如果不用 dummy,就要单独处理头节点。加一个 dummy 后,所有删除都变成“删除 prev.next”:
dummy -> 1 -> 2 -> 3
prev = dummy
无论删除的是第一个真实节点还是中间节点,写法都一致。面试里 dummy 常用于删除节点、合并链表、分隔链表、反转局部链表等场景;关键是记住返回 dummy.next,不要把 dummy 当真实数据返回。
完整版教学
一、链表最容易乱在头节点变化
数组删除第一个元素,返回值通常还是同一个数组;链表删除头节点时,head 指针本身会变。
before: head -> 1 -> 2 -> 3
delete 1
after: head -> 2 -> 3
如果函数内部没有正确更新并返回新头,调用方还拿着旧 head,链表就错了。很多链表 bug 不是指针公式不会写,而是头节点特殊处理漏了。
记忆钩子:dummy 的作用是给真实头节点找一个永远存在的前驱。
二、dummy 让删除头节点和删除中间节点统一
删除节点通常需要知道它的前驱 prev。如果要删除头节点,头节点没有真实前驱,所以要特殊处理。
加 dummy 后:
dummy -> 1 -> 2 -> 3
prev = dummy
删除第一个真实节点和删除中间节点都可以写成:
prev.next = prev.next.next;
这个统一性非常重要。链表题里分支越少,指针断错的概率越低。
三、删除指定值的代码示例
不用 dummy 时,常常要先 while 删除头部连续目标值,再处理后面节点。用 dummy 可以写得更稳定。
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
while (prev.next != null) {
if (prev.next.val == target) {
prev.next = prev.next.next;
} else {
prev = prev.next;
}
}
return dummy.next;
注意循环看的是 prev.next,因为删除动作发生在 prev.next 上。删除后 prev 不前进,否则可能跳过连续目标节点。
四、合并链表时 dummy 让尾插更自然
合并两个有序链表时,我们需要维护新链表的尾指针。如果没有 dummy,第一次插入要单独决定新头。
ListNode dummy = new ListNode(0);
ListNode tail = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
tail.next = l1;
l1 = l1.next;
} else {
tail.next = l2;
l2 = l2.next;
}
tail = tail.next;
}
tail.next = (l1 != null) ? l1 : l2;
return dummy.next;
dummy 在这里像“结果链表的工作台”。真正结果从 dummy.next 开始。
五、dummy 不是必须,但它降低心智负担
理论上所有 dummy 写法都能改成不用 dummy,只是会多出头节点分支。
| 场景 | 不用 dummy 的难点 | 用 dummy 的效果 |
|---|---|---|
| 删除节点 | 头节点被删要特殊处理 | 所有节点都有前驱 |
| 合并链表 | 第一次插入要定头 | tail 从 dummy 开始 |
| 分隔链表 | 两个结果链表头难维护 | 两个 dummy 分别收集 |
| 局部反转 | 子链表前驱可能为空 | dummy 统一前驱 |
面试里使用 dummy 不是偷懒,而是主动规避边界复杂度。
六、dummy 的常见细节
dummy 的值通常无意义,可以写 0、-1 或任意值,因为它不会进入返回结果。
dummy.val is ignored
return dummy.next
如果题目有内存释放要求,dummy 只是临时辅助节点。Java、Python 这类语言交给垃圾回收;C/C++ 里如果动态分配 dummy,要注意释放或使用栈上对象。
另一个细节是不要返回 dummy。返回 dummy 会让结果链表多一个不存在的业务节点。
七、常见误区与追问
- 误区:dummy 是真实链表节点。 dummy 只是辅助节点,最终返回
dummy.next。 - 误区:只有删除头节点才需要 dummy。 合并、分隔、局部反转等需要稳定前驱的场景都适合。
- 误区:用了 dummy 就不用考虑空链表。 空链表时
dummy.next为 null,循环条件仍要写对。 - 追问:删除连续目标值时 prev 为什么不一定前进? 删除后新的
prev.next可能仍是目标值,前进会跳过检查。 - 追问:dummy 会不会增加空间复杂度? 只增加 1 个节点,额外空间仍是 O(1)。
- 追问:什么时候不需要 dummy? 头节点不可能变化、逻辑很简单时可以不用。
八、加强记忆
dummy 可以记成“给头节点补前驱”。链表操作麻烦在头节点可能变化,而 dummy 让第一个真实节点也有前驱,于是删除、合并、分隔、局部反转都能统一成普通节点操作。写完记住两件事:dummy 的值无意义,最终返回 dummy.next。