链表:单链 / 双链 / 环形

本节目标

节点与基本操作:

// 运行环境:Node.js 14+
// 保存为 dsa-l5.js,执行:node dsa-l5.js

class ListNode {
  constructor(val, next = null) { this.val = val; this.next = next }
}

// 遍历
function traverse(head) {
  let cur = head
  while (cur) { console.log(cur.val); cur = cur.next }
}

// 插入:先接后断(顺序不能反!)
function insertAfter(prev, node) {
  node.next = prev.next
  prev.next = node
}

// 删除:跳过目标
function deleteNext(prev) { if (prev.next) prev.next = prev.next.next }

虚拟头节点(dummy):统一"删除头节点"和"在头部插入"的边界处理。

function removeValue(head, val) {
  const dummy = new ListNode(0, head) // 虚拟头,永远不动
  let cur = dummy
  while (cur.next) {
    if (cur.next.val === val) cur.next = cur.next.next // 跳过
    else cur = cur.next
  }
  return dummy.next // 真实新头
}

反转链表(迭代 + 递归):

function reverse(head) {            // 迭代:三指针 prev/cur/next
  let prev = null, cur = head
  while (cur) {
    const next = cur.next
    cur.next = prev
    prev = cur
    cur = next
  }
  return prev
}
function reverseR(head) {           // 递归:子链反转后让原头指向新尾
  if (!head || !head.next) return head
  const newHead = reverseR(head.next)
  head.next.next = head
  head.next = null
  return newHead
}

环形检测(快慢指针 / 龟兔赛跑):

function hasCycle(head) {
  let slow = head, fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
    if (slow === fast) return true
  }
  return false
}

找中间节点:快慢指针,快走 2 慢走 1,快到底时慢在中间。合并两个有序链表(递归最优雅):

function mergeTwo(l1, l2) {
  if (!l1) return l2
  if (!l2) return l1
  if (l1.val < l2.val) { l1.next = mergeTwo(l1.next, l2); return l1 }
  l2.next = mergeTwo(l1, l2.next); return l2
}
// 调用示例
const h = new ListNode(1, new ListNode(2, new ListNode(3)))
console.log(reverse(h).val) // 3(反转后头是 3)

名词解释

课后练习

  1. 反转链表两种写法都过一遍(leetcode 206)。
    • 答案:迭代版见上 reverse;递归版见 reverseR。递归版先反转 head.next 子链得到 newHead,再让 head.next.next = head 把原头接成新尾。
  2. 找环形链表入口节点(142)的思路?
    • 答案:相遇后,让一个指针从 head、另一个从相遇点同时每次走 1 步,再次相遇处即入口。这是因为 head 到入口的距离 = 相遇点到入口的距离(环内绕圈部分抵消)。

总结

链表是检验"会不会真正操作指针"的试金石。数组靠下标随机访问,链表靠 next 指针逐节游走,这个差别决定了它所有经典题的套路:反转靠三指针改写 next,判环靠快慢指针,合并靠递归。初学者最容易栽在"插入时先接后断的顺序"——一旦先断 prev.next 就丢了后半截链表,再也找不回来。虚拟头节点是我强烈推荐的"偷懒"技巧:它把头节点的特殊边界彻底抹平,让你只用一套循环处理所有情况。学链表千万别裸写代码,先在纸上画出节点和指针箭头,再翻译箭头变化成代码,正确率立刻翻倍。链表题做熟了,你对"引用/指针"的理解会上一个台阶,后面学树、图都更顺。