手写链表的反转、合并两个有序链表和查找中间节点。

先理解原理和示例,再练习完整口述与追问。

反转用 prev、cur 两个指针逐个调转 next,先保存后继;合并用哨兵节点 dummy 简化头节点处理,比较两表头较小者接到尾部,最后接上剩余部分;中间节点用快慢指针,快指针每次两步、慢指针一步,偶数长度时返回第二个中间节点。均为 O(n) 时间、O(1) 额外空间。

链表题的关键是修改指针前先保存会丢失的引用。反转时如果先写 cur.next = prev,就失去了原来的后继,所以先 const next = cur.next。哨兵节点让“结果链表为空”和“非空”走同一套逻辑,最后返回 dummy.next。快慢指针循环条件 fast && fast.next,保证 fast.next.next 不会访问 null 的属性;快指针到达末尾时慢指针正好在中间。同样的方法还能判断链表是否有环(快慢指针相遇)。

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

function fromArray(values) {
  const dummy = new ListNode(0)
  let tail = dummy
  for (const value of values) tail = tail.next = new ListNode(value)
  return dummy.next
}

function toArray(head) {
  const values = []
  for (let node = head; node; node = node.next) values.push(node.val)
  return values
}

function reverseList(head) {
  let prev = null
  let cur = head
  while (cur) {
    const next = cur.next
    cur.next = prev
    prev = cur
    cur = next
  }
  return prev
}

function mergeTwoLists(a, b) {
  const dummy = new ListNode(0)
  let tail = dummy
  while (a && b) {
    if (a.val <= b.val) {
      tail.next = a
      a = a.next
    } else {
      tail.next = b
      b = b.next
    }
    tail = tail.next
  }
  tail.next = a || b
  return dummy.next
}

function middleNode(head) {
  let slow = head
  let fast = head
  while (fast && fast.next) {
    slow = slow.next
    fast = fast.next.next
  }
  return slow
}

推演:[1, 2, 3, 4] 中,快指针依次在 1、3、末尾之后,慢指针依次在 1、2、3,返回值为 3 的节点,即第二个中间节点;mergeTwoLists 中相等时取 a 的节点,保持稳定。

反转后忘记原头节点的 next 变为 null 会导致成环(上面的写法第一次迭代即设为 null);合并时遗漏剩余部分;空链表输入未处理。

反转链表用 prev 和 cur 两个指针,每一步先保存 cur.next,再把 cur 指向 prev,然后两个指针前移,最后 prev 就是新头。合并两个有序链表用哨兵节点,每次把较小的节点接到尾部,循环结束后把剩余的那条直接接上,返回 dummy.next。中间节点用快慢指针,快的走两步慢的走一步,快的到末尾时慢的就在中间,偶数长度返回第二个。