面试高频链表题精讲:从哨兵节点到快慢指针
在算法面试中,链表 是一类非常经典的数据结构题型。它不像数组那样支持随机访问,但正因如此,很多操作都需要我们对指针(引用)有精准的控制。本文将通过四道典型题目,带你系统掌握链表处理中的两大核心技巧:哨兵节点(Dummy Node) 和 快慢指针(Fast & Slow Pointers) 。
一、删除链表中的指定节点:为什么需要哨兵节点?
题目描述
给定一个单链表的头节点 head 和一个值 val,删除链表中所有值等于 val 的节点,并返回新的头节点。
力扣:LCR 136. 删除链表的节点 - 力扣(LeetCode)
初步解法(不使用哨兵)
js
function remove(head, val) {
// 特殊处理头节点
if (head && head.val === val) {
return head.next;
}
let cur = head;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next;
break; // 假设只删第一个匹配项
}
cur = cur.next;
}
return head;
}
面试官反问:
"你为什么要单独判断头节点?能不能统一处理?"
这是一个非常关键的问题!因为头节点没有前驱节点,所以当我们想"让前一个节点跳过当前节点"时,头节点就成了特例。
引入哨兵节点(Dummy Node)
哨兵节点是一个人为添加的假节点 ,通常放在链表头部,其作用是消除边界条件的特殊处理。
js
function remove(head, val) {
const dummy = new ListNode(0);
dummy.next = head;
let cur = dummy;
while (cur.next) {
if (cur.next.val === val) {
cur.next = cur.next.next;
break;
}
cur = cur.next;
}
return dummy.next; // 返回真正的头节点
}
✅ 优势:
- 不再需要单独判断头节点是否为待删节点;
- 所有节点都变成了"中间节点",逻辑统一;
- 即使链表为空或全被删完,也能安全返回。
二、反转链表:哨兵 + 头插法
题目描述
反转一个单链表。
解法思路
我们可以利用哨兵节点作为新链表的头 ,然后遍历原链表,每次把当前节点插入到哨兵之后 ------这就是头插法。
js
function reverseList(head) {
const dummy = new ListNode(0); // 哨兵,dummy.next 指向已反转部分的头
let cur = head;
while (cur) {
const next = cur.next; // 1. 保存下一个节点
cur.next = dummy.next; // 2. 当前节点指向已反转部分的头
dummy.next = cur; // 3. 更新反转部分的新头
cur = next; // 移动到原链表下一节点
}
return dummy.next;
}
🔍 关键理解:
dummy始终不动,它的next指向当前已反转链表的头;- 每轮操作把
cur插到最前面,实现"头插"; - 三步操作顺序不能乱,否则会断链。
💡 虽然这道题也可以用递归或双指针(prev/cur)实现,但哨兵+头插法提供了一种清晰、不易出错的迭代思路。
三、判断链表是否有环:从哈希表到快慢指针
题目描述
给定一个链表,判断其中是否存在环。
解法一:哈希表(空间换时间)
js
let hash = new Map();
let temp =head;
while(temp)
{
if(hash.has(temp))
return true;
else
hash.set(temp,1);
temp=temp.next;
}
- 时间复杂度:O(n)
- 空间复杂度:O(n)
面试官追问:
"能不能不用额外空间?"
解法二:快慢指针(Floyd 判圈算法)
核心思想:如果链表有环,快指针(每次走两步)一定会在环内追上慢指针(每次走一步)。
js
function hasCycle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next; // 慢指针走1步
fast = fast.next.next; // 快指针走2步
if (slow === fast) { // 比较引用地址(同一内存)
return true;
}
}
return false;
}
✅ 优势:
- 空间复杂度 O(1);
- 时间复杂度 O(n);
- 是链表环检测的标准解法。
🌟 快慢指针不仅用于判环,还可用于找环入口、求中点、删除倒数第 N 个节点等。
四、删除链表的倒数第 N 个节点:哨兵 + 快慢指针的完美结合
题目描述
给你一个链表,删除倒数第 n 个节点,并返回链表头。
力扣:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)
解法思路
- 使用哨兵节点避免头节点被删的边界问题;
- 快指针先走 n 步;
- 然后快慢指针同步移动 ,当快指针到达末尾时,慢指针正好在倒数第 n 个节点的前一个;
- 执行删除操作。
js
const removeNthFromEnd = function(head, n) {
const dummy = new ListNode(0);
dummy.next = head;
let fast = dummy;
let slow = dummy;
// 快指针先走 n 步
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// 快慢指针一起走,直到 fast 到达最后一个节点
while (fast.next) {
fast = fast.next;
slow = slow.next;
}
// 此时 slow 指向倒数第 n 个节点的前一个
slow.next = slow.next.next;
return dummy.next;
};
🎯 为什么需要哨兵?
- 如果要删除的是头节点 (比如链表长度为 5,n=5),那么
slow需要停在"头节点之前"; - 没有哨兵的话,
slow无法指向null的前驱; - 哨兵确保
slow始终有效,且slow.next可安全删除。
总结:链表面试两大法宝
| 技巧 | 适用场景 | 核心价值 |
|---|---|---|
| 哨兵节点(Dummy) | 删除、插入、反转等涉及头节点的操作 | 消除边界条件,统一逻辑 |
| 快慢指针 | 判环、找中点、倒数第 N 个节点 | O(1) 空间解决距离/位置问题 |
在实际面试中,先写出朴素解法(如哈希表) ,再根据面试官提示优化到快慢指针 ,能体现你的思维层次;而哨兵节点则是写出健壮、简洁代码的关键技巧。
希望这篇文章能帮你打通链表题的任督二脉!下次遇到链表题,先问自己两个问题:
- 要不要加哨兵? → 避免头节点特殊处理
- 能不能用快慢指针? → 解决"倒数"、"中点"、"环"等问题
祝你面试顺利,Offer 收割!🎉