合并两个有序链表:两条排好队的货车,如何合成一条?
每次比较两个车头,把较小节点接到结果链表末尾。真正考的是 next 指针怎么改、tail 怎么走。
一句话: 这题不是重新排序,而是 复用原节点 并重连 next 指针。原 list1、list2 的车厢实体不复制——只是被一根新的挂钩链串了起来。
Step 0 · 初始化 dummy 与 tail
创建临时牵引头 dummy,让 tail 与 dummy 重合。dummy 不属于最终答案,只是为了让后面每次接节点都能统一写成 tail.Next = ...,不必特判第一个节点。
指针状态 · Pointer State
Go 代码 · 同步高亮
mergeTwoLists1func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {2 dummy := &ListNode{}3 tail := dummy4 5 for list1 != nil && list2 != nil {6 if list1.Val <= list2.Val {7 tail.Next = list18 list1 = list1.Next9 } else {10 tail.Next = list211 list2 = list2.Next12 }13 tail = tail.Next14 }15 16 if list1 != nil {17 tail.Next = list118 } else {19 tail.Next = list220 }21 22 return dummy.Next23}为什么仍然正确
如果不放 dummy,第一次接节点要单独处理『谁是 head』;有了 dummy 之后,head 就是 dummy.Next,整段循环只有一条主路径。
没有 dummy 时,第一次接节点要单独写 if head == nil { head = ... } else { tail.Next = ... } 这种分支。引入 dummy 后,head 就是 dummy.Next,每次接节点都统一写 tail.Next = smaller,代码只有一条主路径。
tail 永远指向结果链表的最后一个节点(初始时它和 dummy 是同一个)。它决定了下一个节点该接到哪:tail.Next 就是『下一个挂钩位』。每接入一个节点,tail 必须前移到刚接入的节点,否则下一轮还会改写同一根挂钩,链表会断裂。
只有两边都还有节点时才需要比较谁更小。一旦有一边变成 nil,另一边剩下的部分本身就是有序的(输入前提),并且全 ≥ 当前 tail 值——所以可以一口气整段接上,不需要继续逐个比较。
整个过程没有复制任何节点。list1 和 list2 的所有节点实体保持不动,只是把它们的 next 指针重新指过来,再加上 dummy、tail、l1、l2 几个指针变量。额外内存与输入规模 n 无关。
tail.Next = list1; list1 = list1.Next // ← 漏了 tail 前移
后果:下一轮 tail.Next 还是同一个挂钩位,会把刚接上的节点直接覆盖——结果链表只剩最后一次接的节点,前面全部丢失。
tail.Next = list1; tail = tail.Next // ← 漏了 l1 前移
后果:l1 永远停在原地,while 条件永远成立——死循环。
for l1 && l2 { ... }; return dummy.Next // ← 缺接尾后果:结果会少掉那一侧剩余的所有节点(本例中 list2 的 4 会丢失)。
return dummy
后果:调用方拿到的链表会多出一个值为 0 的假头节点——dummy 本来就不应该出现在答案里。
tail.Next = &ListNode{Val: list1.Val}后果:空间复杂度从 O(1) 退化成 O(m+n),性能损失。链表合并的正确做法是复用原节点、只改 next 指针。