链表高频操作精要:Dummy节点、头插法与快慢指针实战指南

94 阅读6分钟

链表高频操作精要:Dummy节点、头插法与快慢指针实战指南

链表是数据结构中的基础线性结构,其节点通过指针链接。由于链表没有下标索引、头尾边界特殊,很多操作(如删除、反转、找倒数第 N 个)容易因边界条件出错。为此,我们常借助 哨兵节点(dummy 节点)快慢指针 等技巧简化逻辑、提升代码鲁棒性。

💼 面试提示:链表题在算法面试中极为高频(尤其是大厂),不仅考察实现能力,更关注边界处理、代码简洁性、是否修改原结构等细节。以下每个技巧后都附有典型面试变种。


一、哨兵节点(Dummy Node)

1. 什么是 Dummy 节点?

  • 是一个人为添加的假节点,不存储真实数据。
  • 通常放在链表最前面(有时也可用于尾部)。
  • 作用:统一处理头节点等边界情况,避免特殊判断。

2. 应用场景

  • 删除节点(尤其是头节点)
  • 反转链表
  • 删除倒数第 N 个节点

3. 示例对比:删除指定值节点

❌ 不使用 dummy 的写法(需特判头节点)
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;
}
✅ 使用 dummy 的写法(统一处理)
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;
}

💼 面试延伸场景

  • “只给要删除的节点(非尾节点),如何删除?”
    → 无法访问前驱,只能将下一节点值复制过来并跳过它(伪删除)。

    node.val = node.next.val;
    node.next = node.next.next;
    
  • “删除所有值为 val 的节点(不止一个)”
    → 把 break 去掉,继续遍历即可。注意仍需 dummy 处理连续头节点被删的情况。


二、链表反转:Dummy + 头插法

核心思想

  • 利用 头插法 将原链表的每个节点逐个插入到新链表的头部,从而实现反转。
  • 引入 dummy 节点作为虚拟头节点,其 dummy.next 始终指向当前已反转部分的真实头节点
  • 这种方法天然避免了对“原 head 变成尾节点后 next 应为 null”的手动处理,逻辑简洁且边界安全。

实现步骤(三步头插)

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步:更新反转链表的新头
    cur = next;                  // 移动到原链表中的下一个节点
  }

  return dummy.next;
}

⚠️ 为什么必须用 next = cur.next 保存下一个节点?

这是头插法中最关键、也最容易出错的细节,面试官常借此考察你对指针操作的理解深度

❌ 如果省略 next,直接写:
while (cur) {
  cur.next = dummy.next;   // 👈 修改了 cur.next!
  dummy.next = cur;
  cur = cur.next;          // 💥 此时 cur.next 已不是原链表的下一个节点!
}
📌 会发生什么?

假设原链表为 1 → 2 → 3 → null

  1. 第一次循环:

    • cur = 1
    • 执行 cur.next = dummy.next(即 1.next = null
    • 然后 cur = cur.nextcur = null
  2. 循环提前结束!

    • 节点 23 永远无法访问,逻辑上丢失。
    • 最终只反转了第一个节点,返回 [1],而非 [3,2,1]

💡 根本原因cur.next 是通往原链表剩余部分的唯一指针。一旦被覆盖(指向已反转部分),就断开了与原链表的连接,导致遍历中断。

可以把这个过程类比为拆桥重建

  • 你站在当前桥墩 cur 上;
  • 下一座桥墩是 cur.next
  • 如果你先把脚下的桥(cur.next)拆了(指向别处),又没记住下一座在哪,就再也过不去了
  • 所以必须先记下下一座桥墩的位置(next = cur.next ,再动手拆桥。

💼 面试表达建议

当被问到“为什么要有 const next = cur.next”时,可以清晰回答:

“因为在头插过程中,我们会修改 cur.next 指向已反转部分的头(即 dummy.next)。如果不提前保存原链表中 cur 的下一个节点,就会永久丢失对剩余链表的引用,导致只能处理第一个节点。所以必须在修改指针前,先用一个临时变量保存 cur.next,确保遍历能继续进行。”


三、快慢指针(Two Pointers)

1. 基本原理

  • 两个指针同向移动,速度不同(通常 fast 走 2 步,slow 走 1 步)。

  • 适用于:

    • 判断环(Floyd 判圈算法)
    • 找中点
    • 删除倒数第 N 个节点

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

💼 面试延伸场景

  • “如果有环,返回环的入口节点”
    → 快慢指针相遇后,将一个指针重置到 head,两者同速前进,再次相遇即为入口。
    (原理:head 到入口距离 = 相遇点绕环若干圈后到入口的距离)
  • “判断链表是否为回文”
    → 快慢指针找中点 → 反转后半段 → 双指针比较前后两段 → (可选)恢复结构。
    ⚠️ 面试官可能追问:“能否不修改原链表?” → 此时可用栈存前半段。

四、删除链表的倒数第 N 个节点:Dummy + 快慢指针

思路

  • 快指针先走 N 步,慢指针随后同步移动。
  • 当快指针到达末尾时,慢指针正好在倒数第 N 个节点的前一个。
const removeNthFromEnd = function(head, n) {
  const dummy = new ListNode(0);
  dummy.next = head;

  let fast = dummy;
  let slow = dummy;

  for (let i = 0; i < n; i++) {
    fast = fast.next;
  }

  while (fast.next) {
    fast = fast.next;
    slow = slow.next;
  }

  slow.next = slow.next.next;
  return dummy.next;
};

💼 面试延伸场景

  • “删除倒数第 N 个节点,但要求一次遍历且不用 dummy”
    → 可行但复杂:需额外判断 n 是否等于链表长度(即删头节点)。强烈建议坚持用 dummy,并向面试官说明:“这样能统一处理边界,提高代码健壮性。”
  • “找出倒数第 N 个节点的值(不删除)”
    → 同样用快慢指针,最后返回 slow.next.val

五、总结:链表操作三大法宝 + 面试心法

技巧作用典型应用面试加分点
Dummy 节点统一边界处理删除、反转、分组主动说明“避免特判头节点,提升鲁棒性”
头插法构建逆序反转、局部反转能画图解释三步操作,强调“必须先保存 next”
快慢指针定位特定位置判环、找中点、倒数第 N 个能推导环入口原理

💡 面试答题黄金流程

  1. 澄清问题:是否可修改原链表?是否可能为空?是否只删一个?
  2. 画图分析:标出关键指针(prev, cur, next, dummy 等)
  3. 选择技巧:优先 dummy + 快慢指针组合
  4. 写代码:结构清晰,注释关键步骤
  5. 验证边界:举例 [1], [1,2], n=1, val=1

🌟 终极口诀
有边界,加 dummy;要反转,用头插;找位置,快慢指针上;遇删除,想前驱;面链表,先画图!