Dummy 节点联手快慢指针:优雅解决链表中的边界难题

75 阅读5分钟

Dummy 节点联手快慢指针:优雅解决链表中的边界难题

在算法与数据结构的学习中,链表是一种基础但极具代表性的线性结构。由于其动态性和指针特性,链表操作常常涉及复杂的边界条件和指针控制。本文将系统梳理链表操作中的几个核心技巧:哨兵节点(Dummy Node)头插法反转链表快慢指针,并解析三类经典问题:删除指定值节点删除倒数第 N 个节点判断链表是否有环


一、哨兵节点(Dummy Node):简化边界处理

什么是哨兵节点?

Dummy 节点是一个人为添加的、不存储真实数据的假节点,通常被放置在链表的最前面(有时也在尾部)。它的核心目的不是参与数据存储,而是简化链表操作中的边界条件处理

例如,在单向链表中,头节点没有前驱节点,这使得删除或插入操作需要特殊判断。而一旦我们在原链表前加一个 dummy 节点,所有真实节点(包括原来的头节点)就都有了“前驱”,从而可以用统一的逻辑处理所有情况。

🛡️ 简单说:Dummy 节点就像给链表穿上了一件“防护服”,让操作更安全、代码更简洁。

二、Dummy节点应用三大示例

一、删除链表中值为 val 的节点

方法一:不使用哨兵节点(需特殊处理头节点

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;
}
  • 缺点:需单独判断头节点是否为目标值。
  • 逻辑不够统一,易出错。

方法二:使用哨兵节点(推荐)

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;
}
  • 优点:无需区分头节点,逻辑简洁统一。
  • dummy.next 始终指向真正的链表头。

二、 链表反转:头插法 + 哨兵节点

1. 核心思想:头插法

头插法是一种构建或反转链表的常用技巧,其核心思想是:每次将新节点插入到当前链表的头部(即最前面) 。具体做法是,遍历原链表时,把当前节点的 next 指向已有结果链表的第一个真实节点,然后更新结果链表的头指针为当前节点。这样,先访问的节点会逐渐被“推”到后面,后访问的节点则始终位于最前面,最终实现如链表反转等效果。

2. 实现方式:

function reverseList(head) {
  const dummy = new ListNode(0); // 哨兵节点,dummy.next 指向已反转部分的头
  let cur = head;  // 当前要处理的节点
  while (cur) {
    const next = cur.next;        // 先保存下一个节点
    cur.next = dummy.next;        
    dummy.next = cur;             
    cur = next;                   
  }
  return dummy.next;
}

核心三步骤

  • 先将当前节点的next指向已反转部分的头节点
  • 更新dummy的next指向当前节点,成为新的头节点
  • 移动到原链表下一个要处理的节点

关键理解:每一轮操作后,dummy.next 始终是当前已反转链表的头节点。


三、快慢指针:解决“距离”与“环”问题

快慢指针是链表中非常强大的双指针技巧,通常是指两个一前一后的指针,两个指针往同一个方向走,只是一个快,一个慢。

核心思想:

  • 使用两个指针:慢指针(slow)每次走 1 步快指针(fast)每次走 2 步(或更多步,视问题而定);
  • 两者从同一位置出发(通常是链表头),以不同速度向前移动;
  • 利用它们之间的相对速度差来实现如“检测环”、“找中点”、“找倒数第 N 个节点”等操作。

1. 判断链表是否有环

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;
}
  • 若存在环,快指针最终会“追上”慢指针。
  • 若无环,快指针会先到达 null

2. 删除倒数第 N 个节点

思路:让快指针先走 N 步,然后快慢指针同步移动。当快指针到达末尾时,慢指针正好位于倒数第 N 个节点的前一个。

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;
};

优势:只需一次遍历,时间复杂度 O(L),空间 O(1)。用空间换时间,用简单换复杂


总结:Dummy 节点——链表操作的“隐形守护者”

在链表算法中,最令人头疼的往往不是逻辑本身,而是边界情况的处理:头节点删除、空链表插入、前驱缺失……这些细节极易引发特判和错误。而 Dummy 哨兵节点,正是解决这些问题的优雅答案。

Dummy 节点虽然是一个人为添加的、不承载实际数据的虚拟头节点。它不改变链表的本质结构,却为整个操作过程提供了统一的入口和稳定的前驱。无论是删除指定值节点、反转链表,还是结合快慢指针删除倒数第 N 个节点,只要引入 Dummy 节点,原本需要单独处理的头节点就变成了普通节点,所有操作都可以用同一套逻辑完成。

更重要的是,Dummy 节点让代码更简洁、鲁棒、易扩展。可以说,Dummy 节点虽“假”,却是链表操作中最真实的工程智慧。它不炫技,却默默守护着代码的正确性与可读性。当你下次面对链表题犹豫如何处理边界时,请记住:加一个 Dummy 节点,往往就是通往清晰解法的第一步。