D

当前:LC21 · 合并两个有序链表 · 首次出现于 Day 9 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC21

合并两个有序链表:两条排好队的货车,如何合成一条?

每次比较两个车头,把较小节点接到结果链表末尾。真正考的是 next 指针怎么改、tail 怎么走。

list1
124
list2
134
output
112344

一句话: 这题不是重新排序,而是 复用原节点 并重连 next 指针。原 list1、list2 的车厢实体不复制——只是被一根新的挂钩链串了起来。

链表 · 归并双轨货车编组站时间 O(m+n)空间 O(1)
Step 0 / 8
当前发生了什么

Step 0 · 初始化 dummy 与 tail

创建临时牵引头 dummy,让 tail 与 dummy 重合。dummy 不属于最终答案,只是为了让后面每次接节点都能统一写成 tail.Next = ...,不必特判第一个节点。

指针状态 · Pointer State

l1 →
1
l2 →
1
tail →
dummy
已接入
0 个节点
当前 result 链表
(空)
list1list2result1head241head34nilnill1l2dummy不属于答案niltail
list1 节点 / l1 扫描器list2 节点 / l2 扫描器dummy(不属于答案)tail 机械臂result 已接入 / next 挂钩nil 终点

Go 代码 · 同步高亮

mergeTwoLists
1func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
2 dummy := &ListNode{}
3 tail := dummy
4
5 for list1 != nil && list2 != nil {
6 if list1.Val <= list2.Val {
7 tail.Next = list1
8 list1 = list1.Next
9 } else {
10 tail.Next = list2
11 list2 = list2.Next
12 }
13 tail = tail.Next
14 }
15
16 if list1 != nil {
17 tail.Next = list1
18 } else {
19 tail.Next = list2
20 }
21
22 return dummy.Next
23}

为什么仍然正确

如果不放 dummy,第一次接节点要单独处理『谁是 head』;有了 dummy 之后,head 就是 dummy.Next,整段循环只有一条主路径。

必须讲透 · 4 个核心知识点
K1为什么需要 dummy?

没有 dummy 时,第一次接节点要单独写 if head == nil { head = ... } else { tail.Next = ... } 这种分支。引入 dummy 后,head 就是 dummy.Next,每次接节点都统一写 tail.Next = smaller,代码只有一条主路径。

K2tail 到底是什么?

tail 永远指向结果链表的最后一个节点(初始时它和 dummy 是同一个)。它决定了下一个节点该接到哪:tail.Next 就是『下一个挂钩位』。每接入一个节点,tail 必须前移到刚接入的节点,否则下一轮还会改写同一根挂钩,链表会断裂。

K3为什么 while 条件是 l1 && l2?

只有两边都还有节点时才需要比较谁更小。一旦有一边变成 nil,另一边剩下的部分本身就是有序的(输入前提),并且全 ≥ 当前 tail 值——所以可以一口气整段接上,不需要继续逐个比较。

K4为什么空间复杂度是 O(1)?

整个过程没有复制任何节点。list1 和 list2 的所有节点实体保持不动,只是把它们的 next 指针重新指过来,再加上 dummy、tail、l1、l2 几个指针变量。额外内存与输入规模 n 无关。

常见错误 · 写代码时最容易踩的坑
E1忘记 tail = tail.Next
tail.Next = list1; list1 = list1.Next  // ← 漏了 tail 前移

后果:下一轮 tail.Next 还是同一个挂钩位,会把刚接上的节点直接覆盖——结果链表只剩最后一次接的节点,前面全部丢失。

E2忘记 list1 = list1.Next(或 list2 同理)
tail.Next = list1; tail = tail.Next  // ← 漏了 l1 前移

后果:l1 永远停在原地,while 条件永远成立——死循环。

E3while 退出后忘记接剩余链表
for l1 && l2 { ... }; return dummy.Next  // ← 缺接尾

后果:结果会少掉那一侧剩余的所有节点(本例中 list2 的 4 会丢失)。

E4return dummy 而不是 dummy.Next
return dummy

后果:调用方拿到的链表会多出一个值为 0 的假头节点——dummy 本来就不应该出现在答案里。

E5误以为要 new 一堆新节点
tail.Next = &ListNode{Val: list1.Val}

后果:空间复杂度从 O(1) 退化成 O(m+n),性能损失。链表合并的正确做法是复用原节点、只改 next 指针。

步骤时间线 · 9