告别边界条件:哨兵节点(Dummy)与快慢指针的“链表降维打击”

70 阅读5分钟

哈喽,各位正在算法之路上摸爬滚打的小伙伴们!👋

链表(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. 算法思路

  1. 准备阶段:创建 dummy,让 fastslow 都指向它。
  2. 制造间距:让 fast 先跑 N 步。此时 fastslow 之间隔了 N 个节点。
  3. 同步推进:两个指针一起跑,直到 fast 到达末尾。
  4. 精准打击:此时 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;
}

结语:给新手的“避坑”指南

  1. 看到链表先想 Dummy:只要涉及增删、调整顺序,尤其是可能动到头节点的情况,先写一行 const dummy = new ListNode(0); dummy.next = head; 准没错,这能帮你省去 80% 的 if(head === null) 判断。

  2. 画图、画图、还是画图:链表的指针指向极其抽象,写代码前在纸上画出 curnext 的指向变化,能有效避免“断链”或“死循环”。

  3. 注意 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?