从面试官视角审视单链表:Dummy节点、快慢指针与经典操作
大家好,今天来讲讲在面试中经常考察的链表相关题目。为什么链表这么受欢迎?因为它能很好地考察候选人对指针操作的掌握、对边界条件的处理能力,以及对底层逻辑的理解。链表不像数组有随机访问,操作时稍有不慎就会出现空指针、死循环或内存泄漏,这些都是面试中容易暴露的问题。
今天,我们以几个经典题目为核心,深入剖析Dummy哨兵节点、快慢指针以及链表反转等技巧。这些技巧几乎覆盖了80%的链表面试题。内容基于LeetCode高频题(如203、206、19、141等),结合实际代码和图解,帮助你彻底吃透链表。
一、Dummy哨兵节点:统一边界,优雅删除
在处理链表删除时,最头疼的就是头节点可能被删除的情况。如果没有特殊处理,代码会多出一堆if判断,非常丑陋。
Dummy节点(也叫哨兵节点)就是一个假节点,值随意(通常设为0或-1),它的next指向原head。这样,整个链表就都有了“前驱节点”,删除操作统一了。
经典题目:删除链表中所有等于val的节点(LeetCode 203)
原始代码容易出错的版本(不使用Dummy):
function remove(head, val) {
// 特殊处理头节点
while (head && head.val === val) {
head = head.next;
}
let cur = head;
while (cur && cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
你看,头节点需要单独处理,代码冗长。
使用Dummy后的优雅版本:
function remove(head, val) {
const dummy = new ListNode(0);
dummy.next = head;
let cur = dummy; // 从dummy开始遍历
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next; // 删除下一个节点
} else {
cur = cur.next;
}
}
return dummy.next; // 新头节点
}
底层逻辑:Dummy相当于给原链表加了一个“保险前驱”。无论删除头节点还是中间节点,操作都是cur.next = cur.next.next,完美统一。
易错提醒:
- 忘记返回
dummy.next,直接返回head会导致头节点丢失。 - 在循环条件里用
cur.next而非cur,防止cur已经是null时访问cur.next。 - 如果链表可能为空,直接返回dummy.next就行,无需特殊判断。
面试扩展:如果要删除所有重复元素保留一个(LeetCode 83),或删除所有重复元素一个不留(LeetCode 82),同样可以用Dummy统一处理。
二、链表反转:头插法 + Dummy的完美结合
反转链表(LeetCode 206)是面试必考。常见有三种实现:三指针迭代、递归、头插法。
这里重点讲头插法 + Dummy,因为它最直观,也最容易扩展到局部反转(LeetCode 92)。
function reverseList(head) {
const dummy = new ListNode(0); // dummy作为新链表的“头”
let cur = head;
while (cur) {
const next = cur.next; // 1. 保存原链表下一个节点
cur.next = dummy.next; // 2. 当前节点指向已反转部分的头
dummy.next = cur; // 3. dummy.next成为新头(头插)
cur = next; // 移动到原链表下一个
}
return dummy.next;
}
底层逻辑图解(想象链表1→2→3→4→5):
- 初始:dummy → null
- 处理1:cur=1 → dummy → 1 → null
- 处理2:cur=2 → dummy → 2 → 1 → null
- 处理3:cur=3 → dummy → 3 → 2 → 1 → null
- ...
- 最终:dummy → 5 → 4 → 3 → 2 → 1 → null,返回dummy.next即为反转后链表
这就是经典的“头插法”。每处理一个节点,就把它插到dummy后面,成为新的头节点。
易错提醒:
- 必须先保存
cur.next,否则断了链就找不回来了。 - 三步顺序不能乱:先改cur.next,再改dummy.next。
- 递归实现虽然代码短,但栈深度O(n),长链表可能栈溢出,生产环境慎用。
面试扩展:局部反转(反转从第m到第n个节点),同样用Dummy找到m的前一个节点,然后在[m,n]区间用头插法反转即可。
三、快慢指针:解决“倒数”和“环”问题
快慢指针是链表中最强大的技巧之一。两个指针,一个快一个慢,共同出发。
1. 判断链表是否有环(LeetCode 141)
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) { // 快指针能走就走两步
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true; // 相遇即有环
}
return false;
}
底层逻辑:像操场跑圈,快指针每次两步,慢指针一步。如果有环,快指针必然会“追上”慢指针(在环内相遇)。如果无环,快指针先到末尾。
易错提醒:
- 判断条件是
fast && fast.next,防止fast为null或fast.next为null时访问。 - 相遇判断用
===,比较的是节点地址,不是val值。
2. 删除链表的倒数第N个节点(LeetCode 19)
这题完美结合了Dummy + 快慢指针。
function removeNthFromEnd(head, n) {
const dummy = new ListNode(0);
dummy.next = head;
let slow = dummy;
let fast = dummy;
// 快指针先走n+1步(因为要让slow停在待删节点的前一个)
for (let i = 0; i <= n; i++) {
fast = fast.next;
}
// 快慢一起走,直到fast为null
while (fast) {
slow = slow.next;
fast = fast.next;
}
// slow.next就是要删的节点
slow.next = slow.next.next;
return dummy.next;
}
底层逻辑:快指针先领先n+1步,当快指针走到末尾(null)时,慢指针正好在倒数第n+1个节点,即待删节点的前驱。配合Dummy,即使删除头节点也无压力。
易错提醒:
- 快指针要先走n+1步(不是n步),否则slow会停在待删节点上。
- n是合法的(题目保证),无需判断n>链表长度。
四、面试官视角:我为什么爱考链表?
- 指针功底:链表操作全是指针改来改去,一不小心就空指针或死循环。
- 边界处理:空链表、单节点、头尾节点删除……这些都是雷区。
- 代码规范:是否用Dummy简化代码?是否考虑内存泄漏(JS不用担心,但C/C++要free)?
- 扩展能力:给你一个题,能否举一反三?比如从删除一个值到删除重复值,从全局反转到局部反转。
备战建议:
- 熟练掌握Dummy + 快慢指针 + 头插法这三大武器。
- 多画图!链表题必须手动画指针移动过程。
- 代码写完后,用极端案例验证:空链表、单节点、两个节点、全相同值等。
链表并不难,难在细节和边界。掌握了这些技巧,面试官再考链表,你也能游刃有余。