哈喽,各位正在算法之路上摸爬滚打的小伙伴们!👋
链表(Linked List),这个在数据结构面试中出镜率高达 90% 的“常客”,想必让不少人心碎过。为什么?因为它实在太“矫情”了:
- “删除节点?先判断是不是头节点。”
- “反转链表?哎呀,断开了,后面的节点找不到了。”
- “找倒数第 N 个?救命,长度还没算出来。”
别急!今天我们就祭出两把“手术刀”——哨兵节点(Dummy Node)和快慢指针(Slow & Fast Pointers) 。掌握了它们,你就能像拆解积木一样优雅地处理链表。
第一章:哨兵节点(Dummy)—— 链表界的“万能胶水”
1. 为什么要用它?
新手在写链表删除或反转时,最容易写出这样的逻辑:
JavaScript
if(head && head.val === val){
return head.next; // 特殊处理头节点
}
这种代码写多了,逻辑会变得非常臃肿。头节点之所以特殊,是因为它没有前驱节点。 哨兵节点(Dummy Node) 的核心思想就是:人为地给头节点创造一个“前任”。 它是我们为了简化边界条件而添加的一个假节点,不存储真实数据,通常指向原链表的 head。
2. 实战演练:删除指定值的节点
如果没有 Dummy 节点,我们要判断删除的是不是第一个;有了它,所有节点的处理逻辑都统一了。
JavaScript
function remove(head, val) {
// 1. 创建一个虚假的哨兵,无论原链表是否为空,dummy 始终存在
const dummy = new ListNode(0);
dummy.next = head;
// 2. cur 从 dummy 开始遍历,这样我们处理任何节点都有前驱节点了
let cur = dummy;
while(cur.next) {
if(cur.next.val === val) {
// 核心逻辑:直接跳过目标节点
cur.next = cur.next.next;
break; // 找到并删除了,功成身退
}
cur = cur.next;
}
// 3. 记住!最后返回的是 dummy.next,这才是真正的、新的头节点
return dummy.next;
}
第二章:链表反转与“头插法”—— 空间重组的艺术
反转链表是面试必考题。很多同学喜欢用递归,但在面试高压下,递归容易写晕。这里推荐一种极其稳健的方法:Dummy 节点 + 头插法。
1. 什么是头插法?
想象你手里有一串珠子(A-B-C),你想把它们反转过来。
头插法的逻辑是:每次拿出一个新珠子,都强行塞到 Dummy 节点的屁股后面。
2. 核心三部曲(重点,敲黑板!)
我们来看看代码中那几个关键的“转圈圈”步骤:
JavaScript
function reverseList(head) {
const dummy = new ListNode(0); // 它的 next 将始终指向当前已反转部分的“领头羊”
let cur = head; // 当前要处理的“倒霉蛋”节点
while(cur) {
// 第一步:保存“后路”
// 因为一会儿 cur 的 next 要被改掉,如果不存,cur 之后的兄弟们就丢了
const next = cur.next;
// 第二步:执行“头插”
// 让当前节点的 next 指向 dummy 后面的那个人(已反转部分的头部)
cur.next = dummy.next;
// 第三步:更新 dummy 的指向
// 让 dummy 重新指向新插进来的这个 cur,cur 正式成为“新头”
dummy.next = cur;
// 移动到下一个节点,继续重复
cur = next;
}
return dummy.next;
}
底层逻辑总结:
cur.next = dummy.next:这一步是把当前节点和已经反转好的那部分“粘”起来。dummy.next = cur:这一步是告诉 Dummy ,“我又是最新的老大了”。
第三章:快慢指针(Slow & Fast)—— 链表界的“距离大师”
快慢指针是解决链表效率问题的核心武器。顾名思义,两个指针同时出发,但步长不同。
1. 判断环形链表(弗洛伊德判圈算法)
如果一个链表有环,就像操场跑道。快指针(Fast,每次走两步)和慢指针(Slow,每次走一步)最终一定会相遇。
JavaScript
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; // 快指针跑到终点了,说明没环
}
第四章:终极挑战——删除倒数第 N 个节点
这是一个经典的“组合技”:Dummy 节点 + 快慢指针。
1. 难点分析
要删除倒数第 N 个,传统做法是遍历一遍拿长度 L,再正向走 L-N 步。能优化吗?能!用快慢指针制造**“时间差”**。
2. 算法思路
- 准备阶段:创建
dummy,让fast和slow都指向它。 - 制造间距:让
fast先跑N步。此时fast和slow之间隔了N个节点。 - 同步推进:两个指针一起跑,直到
fast到达末尾。 - 精准打击:此时
slow恰好停在待删除节点的前一个位置。
3. 代码实现
JavaScript
const removeNthFromEnd = function(head, n) {
const dummy = new ListNode(0);
dummy.next = head;
let fast = dummy;
let slow = dummy;
// 第一步:快指针先行 N 步
// 这样快慢指针之间就有了 N 的间隔
for (let i = 0; i < n; i++) {
fast = fast.next;
}
// 第二步:当 fast.next 为空时,fast 到了最后一个节点
// 此时 slow 刚好在倒数第 N 个节点的前一个!
while(fast.next) {
fast = fast.next;
slow = slow.next;
}
// 第三步:跨越式删除
// 慢指针指向的就是倒数第 N 个结点的前一个,直接指向下下个即可
slow.next = slow.next.next;
return dummy.next;
}
结语:给新手的“避坑”指南
-
看到链表先想 Dummy:只要涉及增删、调整顺序,尤其是可能动到头节点的情况,先写一行
const dummy = new ListNode(0); dummy.next = head;准没错,这能帮你省去 80% 的if(head === null)判断。 -
画图、画图、还是画图:链表的指针指向极其抽象,写代码前在纸上画出
cur、next的指向变化,能有效避免“断链”或“死循环”。 -
注意
while终止条件:while(fast && fast.next)常用于找中点或判环。while(fast.next)常用于找最后一个节点。
恭喜你!到这里你已经掌握了链表算法的半壁江山。哨兵节点保安全,快慢指针控节奏,多加练习,你也能在链表的世界里游刃有余!
想看更多关于链表中点、合并有序链表的技巧吗?或者对哪行代码还有疑惑?欢迎在评论区留言讨论,我们一起进步!🚀
Would you like me to generate some practice problems related to these concepts for you to try?