前言
最近把链表相关的高频题又捋了一遍,感觉很多操作其实都围绕着几个核心技巧:dummy 哨兵节点、头插法、快慢指针。这些技巧一旦掌握,链表题基本就无敌了。
我把自己的笔记整理了一下,顺便加了一些细节和心得,分享给大家。内容从最基础的删除节点开始,逐步到反转、判环、删除倒数第 N 个节点,由浅入深,代码都用 JavaScript 写的
1. 删除链表中的某个值(LeetCode 203 移除链表元素)
最常见的操作之一:给一个值,把链表中所有等于这个值的节点删掉。
一开始我总是忘了考虑“要删的是头结点”的情况,因为头结点没有前一个节点,处理起来很麻烦。
解决办法:加一个 dummy 哨兵节点
function remove(head, val) {
// dummy 节点的值无所谓,next 指向原头结点
const dummy = new ListNode(0, 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 这么好用? 它统一了所有节点的删除逻辑:每个要删的节点都有“前一个节点”(哪怕原来是头结点)。以后看到要修改头结点的操作,第一时间就该想到 dummy。
小 tip:很多链表题的标配就是先 new 一个 dummy,养成习惯就行。
2. 链表反转(LeetCode 206 反转链表)—— 头插法的经典应用
反转链表是我觉得最优雅的题目之一,而头插法就是它的灵魂。很多同学一看到反转就头大,其实掌握了头插法之后,反转、局部反转、甚至一些排序题都能轻松应对。
什么是头插法?
头插法(Head Insertion)是一种构建链表的方式:每次把新节点插入到链表的最前面,而不是尾部。
在反转链表里,我们正是利用这个思路:把原链表的节点一个一个“摘下来”,然后用头插法插入到一个新的链表中,自然就实现了反转。
为什么需要 dummy?
我们用一个 dummy 节点作为新链表的“假头”。它的 next 永远指向当前已经反转好的部分的最前面(即新链表的头结点)。这样操作起来特别方便,不用每次都去判断新链表是不是空的。
代码实现
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. 更新 dummy.next,让当前节点成为新头
cur = next; // 继续处理原链表的下一个节点
}
return dummy.next;
}
手推一遍过程(链表 1→2→3→4)
- 初始状态: 原链表:1→2→3→4→null dummy → null
- 处理 1: next = 2 1 → null(断开原连接) 1 → dummy.next(null) dummy → 1 → null cur = 2
- 处理 2: next = 3 2 → 1 dummy → 2 → 1 → null cur = 3
- 处理 3: dummy → 3 → 2 → 1 → null
- 处理 4: dummy → 4 → 3 → 2 → 1 → null
最终返回 dummy.next,就是反转后的链表。
头插法的核心三行代码(一定要背下来!)
const next = cur.next; // 保存后继
cur.next = dummy.next; // 指向当前新头的下一个(插入)
dummy.next = cur; // 更新新头
cur = next; // 继续前进
这三行几乎是所有头插法操作的模板,后续遇到局部反转(比如 LeetCode 92 反转链表 II)、按区间反转、甚至重排链表(143)等题,都可以直接套用。
掌握了头插法,你会发现反转不再是难题,而是变成了一种“乐趣”。
3. 判断链表是否有环(LeetCode 141 环形链表)
这个经典得不能再经典:快慢指针。
- 慢指针每次走 1 步
- 快指针每次走 2 步
如果有环,快指针总会追上慢指针(在环里绕圈)。
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 先到末尾,无环
}
为什么一定能相遇? 想象操场跑步,快的总会套圈追上慢的。数学上可以用“鸽巢原理”解释,但直觉就够用了。
4. 删除链表的倒数第 N 个节点(LeetCode 19 删除链表的倒数第 N 个节点)
这题结合了 dummy + 快慢指针,堪称链表技巧大合集。
思路:让快指针先走 n 步,然后快慢一起走,当快指针到末尾时,慢指针正好在倒数第 n 个节点的前一个。
var removeNthFromEnd = function(head, n) {
const dummy = new ListNode(0, head);
let left = dummy;
let right = head; // 注意这里可以直接从 head 开始
// 快指针先走 n 步
while (n--) {
right = right.next;
}
// 一起走,直到快指针到末尾
while (right) {
left = left.next;
right = right.next;
}
// left 现在指向倒数第 n 个节点的前一个
left.next = left.next.next;
return dummy.next;
};
为什么 right 从 head 开始,而 left 从 dummy? 因为我们要删除节点,需要操作它的前一个指针。加 dummy 保证即使删除的是原头结点也没问题。
这题一次性遍历,时间复杂度 O(L),非常优雅。
总结:链表题的三大神器
- dummy 哨兵节点 → 解决头结点特殊处理问题,几乎所有修改链表结构的题都推荐加一个。
- 头插法(三行核心代码) → 反转、局部反转、重新排序等操作的神器,记住了就能秒很多题。
- 快慢指针 → 环检测、找中间节点、删除倒数第 n 个……凡是跟“相对位置”有关的,都可以考虑。
掌握了这三个技巧,链表题基本就从“怕”变成了“喜欢”。下次再刷链表相关题目的时候,先问自己三个问题:
- 要不要加 dummy?
- 能不能用头插法?
- 能不能用快慢指针?
答完这三个问题,代码基本就出来了。