哨兵节点与快慢指针解决链表算法难题

哨兵节点与快慢指针:轻松破解链表算法题🔗

在链表相关的算法题中,边界条件处理往往是最让人头疼的 ------ 比如删除头节点、链表为空、环的判断等。今天就来聊聊两个 "神器":哨兵节点(dummy node)快慢指针,它们能帮我们优雅地解决这些问题,让链表操作变得简单起来!

一、删除链表节点:传统做法的 "小麻烦"📌

先从最基础的 "删除链表中值为 val 的节点" 说起。传统做法通常需要分两种情况处理:

  1. 若要删除的是头节点,直接返回head.next即可;
  2. 若要删除的是中间节点,需要遍历链表找到目标节点的前驱,再修改指针跳过目标节点。

来看一段传统做法的代码:

javascript

复制代码
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;
}

代码解释

  • 先判断头节点是否为目标节点,若是则直接返回下一个节点;
  • cur指针遍历链表,通过cur.next判断是否为目标节点,找到后修改指针完成删除。

缺点 :需要单独处理头节点的情况,逻辑不够统一。如果链表为空(headnull),还可能出现空指针异常。这时候,"哨兵节点" 就要登场了!

二、哨兵节点(dummy):简化边界的 "万能钥匙"🔑

哨兵节点是一个不存储真实数据的假节点 ,通常放在链表的最前面(偶尔也在尾部)。它的核心作用是:消除头节点的特殊性,让所有节点的操作逻辑保持一致

用哨兵节点优化删除操作

来看优化后的代码:

javascript

复制代码
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; // 哨兵节点的next就是新的头节点
}

代码解释

  • 创建dummy哨兵节点,让它的next指向原链表的头节点,此时原头节点和其他节点一样,都有了前驱(dummy);
  • curdummy开始遍历,通过cur.next判断目标节点,找到后直接修改cur.next即可完成删除,无论目标节点是头节点还是中间节点,逻辑完全一致;
  • 最后返回dummy.next,避免了头节点被删除后返回null的处理难题。

有了哨兵节点,再也不用为 "头节点是否存在""是否删除头节点" 这些问题纠结了,是不是很方便?😉

三、LeetCode 206. 反转链表:哨兵节点 + 头插法💫

反转链表是经典问题,用 "哨兵节点 + 头插法" 可以高效解决。头插法的核心是:把原链表的节点依次 "摘" 下来,插入到新链表的头部,而哨兵节点可以作为新链表的 "锚点"。

来看代码:

javascript

复制代码
function reverseList(head) {
    const dummy = new ListNode(0); // 哨兵节点,其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哨兵节点初始时nextnull(因为刚开始反转部分为空);

  • 遍历原链表的每个节点cur,分三步操作:

    1. 先用next保存cur的下一个节点(否则后续步骤会覆盖cur.next,导致原链表丢失);
    2. curnext指向dummy.next(即已反转部分的头节点,第一次循环时指向null);
    3. dummy.next指向cur,此时cur成为新的反转头;
  • 循环结束后,dummy.next就是反转后链表的头节点。

通过哨兵节点,我们不用单独处理 "第一个节点反转后指向 null" 的边界情况,逻辑清晰又简洁!

四、快慢指针:解决链表 "距离" 与 "环" 的利器🏃

快慢指针是另一种处理链表问题的经典技巧:两个指针往同一个方向移动,快指针速度比慢指针快(通常快 1 倍)。它能解决诸如 "环的判断""倒数第 N 个节点" 等涉及 "距离" 或 "循环" 的问题。

LeetCode 141. 环形链表:用快慢指针找环

判断链表是否有环,用快慢指针再合适不过了 ------ 就像跑步时,快的人总会追上慢的人(如果赛道是环形的话)。

来看代码:

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; // 快指针到尾部,无环
}

代码解释

  • 初始时slowfast都指向头节点;

  • 循环中,slow每次走 1 步,fast每次走 2 步:

    • 若链表无环,fast会先到达末尾(fastfast.nextnull),循环结束,返回false
    • 若链表有环,fast会在环内循环,最终追上slow(因为速度快),此时slow === fast,返回true

这个逻辑就像 "套圈"------ 只要赛道是环形,快的人一定能追上慢的人,是不是很形象?😎

五、哨兵节点 + 快慢指针:综合运用的威力💥

LeetCode 141. 环形链表:用快慢指针找环

有些问题需要两种技巧结合,比如 "删除链表的倒数第 N 个节点"(LeetCode LCR 021)。既要找到倒数第 N 个节点,又要处理删除头节点的边界情况,这时候 "哨兵 + 快慢" 就是绝配!

来看代码:

javascript

复制代码
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;
    }

    // 快慢指针一起移动,直到快指针到末尾
    while (fast.next) {
        fast = fast.next;
        slow = slow.next;
    }

    // 慢指针指向倒数第N个节点的前驱,直接删除
    slow.next = slow.next.next;

    return dummy.next; 
}

代码解释

  • 哨兵节点dummy的作用 :避免删除头节点时的特殊处理(比如链表只有 1 个节点,删除后返回null),同时确保slow.next.next一定有效(不会出现空指针)。

  • 快慢指针的配合

    1. 快指针先移动 N 步,此时快慢指针之间相差 N 个节点;
    2. 两者同时移动,当快指针到达链表末尾(fast.nextnull)时,慢指针恰好指向 "倒数第 N 个节点的前驱";
    3. 直接修改slow.next即可删除目标节点。

通过两者结合,既高效找到目标节点(只需遍历一次链表),又完美处理了边界情况,简直是 "1+1>2" 的效果!

六、面试官可能会问这些问题🤔

  1. 哨兵节点的核心作用是什么?

答:消除头节点的特殊性,让所有节点的操作逻辑统一,简化边界条件(如空链表、删除头节点等)。

  1. 反转链表时,为什么要用头插法 + 哨兵节点?

答:头插法能高效反转(时间复杂度 O (n)),哨兵节点作为 "锚点",避免处理 "第一个节点反转后指向 null" 的特殊逻辑。

3.环形链表中,快慢指针为什么一定会相遇?

答:若有环,快指针速度是慢指针的 2 倍,相对速度为 1 步 / 次,最终会追上慢指针(类似套圈)。

  1. 删除倒数第 N 个节点时,快慢指针的距离为什么是 N?

答:快指针先移 N 步,两者同步移动后,快指针到末尾时,慢指针刚好在目标节点的前驱(距离末尾 N 个节点)。

七、结语🌟

链表问题的难点往往在于边界条件,而哨兵节点和快慢指针就像两把 "钥匙",能帮我们打开这些难题的大门:

  • 哨兵节点让 "头节点" 不再特殊,统一操作逻辑;
  • 快慢指针轻松解决 "环" 和 "距离" 相关问题,降低时间复杂度。

掌握这两个技巧后,再遇到链表题时,不妨先想想:"能不能用哨兵节点简化边界?""快慢指针能帮我找到目标位置吗?" 多加练习,就能让链表操作变得像 "搭积木" 一样简单啦!🚀

相关推荐
这是个栗子3 小时前
【前端知识点总结】前端跨域问题
前端·跨域·cors
拉姆哥的小屋3 小时前
从400维向量到160000维矩阵:基于深度学习的火焰参数预测系统全解析
开发语言·人工智能·python·深度学习·线性代数·算法·矩阵
尽欢i3 小时前
踩过坑才懂:前端生成唯一 ID,别用 Date.now ()了!一行代码搞定
前端·javascript
JS_GGbond3 小时前
解锁 JavaScript 对象的“魔法宝箱”:这些方法让你玩转对象操作
前端·javascript
矢鱼3 小时前
python中对应c++容器的结构
开发语言·c++·python·算法
Doris8933 小时前
【JS】JS进阶--编程思想、面向对象构造函数、原型、深浅拷贝、异常处理、this处理、防抖节流
开发语言·javascript·ecmascript
古城小栈3 小时前
Java 内存优化:JDK 22 ZGC 垃圾收集器调优
java·python·算法
是杉杉吖~3 小时前
《5 分钟上手 React Flex 容器:从 0 搭建响应式卡片列表》
前端·react.js·前端框架
福大大架构师每日一题3 小时前
rust 1.92.0 更新详解:语言特性增强、编译器优化与全新稳定API
java·javascript·rust