合并两个有序链表:哨兵垫头,谁小接谁
两链都升序,下一颗最小节点只可能是某个头。dummy 让第一次接线不用特判空头。
用哨兵 dummy 当结果链的假头,tail 永远指着已接好的尾巴。l1、l2 都还在时,比较两头,把较小节点接到 tail 后面,该链前进一格,tail 跟着走到新尾。一条走完就把另一条剩余整段接上。返回 dummy.Next。时间 O(m+n),空间 O(1)。
这是 LeetCode 21. Merge Two Sorted Lists。给你两条升序链表的头 l1、l2,把它们合成一条升序链表,返回新头。节点可以复用,不必按值新建;空链与另一条合并,结果就是那条还在的链。
主例 l1 = 1→2→4,l2 = 1→3→4,答案 1→1→2→3→4→4。两个 1 谁先接都行,演示里相等取 l1。空链、单节点、一条先耗尽,都必须落在同一套循环里。
每次从两条链里重新找全局最小也能做对,但已经接出去的节点不会再回来,剩下的最小值只可能在两个头上。缺的不是最终序列,而是「下一颗该接谁」的局部判定,以及第一次接线时结果链还是空的那条特判。本文回答:为什么只比两个头就够,以及 dummy 消掉了哪条分支。
核心循环:取更小的头
第一直觉是每次把两条链扫一遍,找出还没接过的最小节点。主例一共六个节点,这样做结果能对,但已经接出去的前缀再也不会参与比较。两条链各自有序,所以各自剩下的那段里,后面的节点都 ≥ 自己的头。全局下一颗最小,只可能是 l1 的头或 l2 的头。
缺的是一个可以循环维持的小事实:任何时刻,已经接到结果里的那段已经有序,并且用尽了所有更小的值;下一颗只要在两个头里选较小的,接到尾巴上,该链前进一格。相等时取哪边都得到同一条值序列,演示约定取 l1。
用手走主例,对应演示的比较帧。pa=0、pb=0,1 对 1,取 l1 的 1,merged = [1]。下一对是 2 对 1,取 l2 的 1,merged = [1, 1]。再下一对是 2 对 3,取 l1 的 2,merged = [1, 1, 2]。再下一对是 4 对 3,取 l2 的 3,merged = [1, 1, 2, 3]。l1 剩 4,l2 剩 4,两段都已有序且都 ≥ 3,整段接到尾巴上,得到 1→1→2→3→4→4。
一条链先走完时,不必再一个一个比。剩下那条的头已经是还没消耗的最小值,它后面保持升序,直接挂到 tail.Next 即可。主例最后两个 4 就是这样拼上去的,演示最后一帧直接给出完整结果。
为什么只比两个头未消耗部分的最小值只可能在头上。后面的节点即使看起来「也可能小」,也必须先经过自己的头,而头已经被证明 ≥ 刚才接出去的值。
哨兵节点:消除「结果链为空」的分支
谁小接谁已经够用,但第一次接线会分叉。结果链还是空的时候,没有 tail 可接,只能写成 head = 较小者;从第二个节点起才变成 tail.Next = 较小者。两条写法服务同一件事,空链、单节点还要再补几条 if。缺的是一个永远存在的「假头」,让第一次和以后每一次都写同一句。
dummy 是一颗不进入答案的占位节点,tail 从 dummy 起步。循环里每一次都是 tail.Next = 选中的节点,再 tail = tail.Next。循环结束后,把还没走完的那条整段赋给 tail.Next。返回 dummy.Next,假头被跳过;若两条都是空,dummy.Next 是 nil,也正确。
演示停在 dummy → 1 → 1 → 2 → 3、tail 指向 3。3 已经就位,下一步把剩余的 4→4 挂到 3 后面即可。哨兵没有改「谁小接谁」这条规则,它只是把空头特判从主循环里拿掉。递归写法本质相同:每次返回较小头,并把它的 Next 接到「剩下两段合并」的结果上,空间却是 O(m+n) 的调用栈。
Go:哨兵 + 双指针
func mergeTwoLists(l1, l2 *ListNode) *ListNode {dummy := &ListNode{}tail := dummyfor l1 != nil && l2 != nil {if l1.Val < l2.Val {tail.Next = l1l1 = l1.Next} else {tail.Next = l2l2 = l2.Next}tail = tail.Next}if l1 != nil { tail.Next = l1 } else { tail.Next = l2 }return dummy.Next}
1dummy 不进答案。tail 从 dummy 起步,第一次接线也是 tail.Next = 节点,不必判断结果链是否为空。
2循环条件是两条都还在。只比两个头:l1.Val < l2.Val 接 l1,否则接 l2。相等走 else,与演示「取 l1」相反也能得到同一条值序列。
3接上之后必须让被接的那条前进,再让 tail 走到新尾,否则下一轮会重复接同一个节点。
4循环结束时至少一条已空。剩下那条整段挂上;两条都空则两边都是 nil,tail.Next 仍是 nil。
5返回 dummy.Next。调用方拿到的是第一颗真实节点,或空链的 nil。
总结
有序 ⇒ 下一颗最小只在两个头上;dummy 让每次都是接尾巴。主例接到 1→1→2→3→4→4。
- 只比两个头,是因为各自剩下的后面都 ≥ 自己的头。主例四次比较之后,两个 4 整段拼接。
- dummy 消掉的是「结果链还空着」那条分支,不是比较规则本身。
- 一条走完不要再逐个比,剩余整段已经有序。递归版同构,迭代版空间 O(1)。