合并两个有序链表 图解题解
两条有序链表各出一个头节点比大小,小的接到结果链末尾——双指针穿针引线,一遍合并完成。
合并两列已按号排好的队伍,不用全部打散重排:两队各出一个人站在前面比号码,号小的跟在结果队尾(tail.next 指向它),然后 tail 前进、那队再出下一个。每次只看两个头,比完接走、指针前移,走完两队长度之和就拼好了,O(1) 额外空间,dummy 头节点免去处理空结果的特判。
这道题到底在问什么
- 输入
- A=1→3→5→7, B=2→4→6
- 输出
- 1→2→3→4→5→6→7
最优解:为什么这么做
一句话答案:LeetCode 21 合并两个有序链表的标准解法是哑结点加双指针归并:每次比较两条链当前的头节点,把较小者接到结果尾部并让那条链前移,一条走空后把另一条剩余整段接上。每个节点恰好被接一次,时间 O(m+n)、空间 O(1),全程复用原节点不新建。
这道题真正在问什么
给两条各自升序的链表,把它们合并成一条仍然升序的链表并返回头节点。注意合并是「拼接原有节点」而不是「新造一条链」——理想做法应该只改各节点的 next 指向,不额外分配节点,这决定了空间可以做到 O(1)。
为什么每次只比较两个头就够了
问题的核心观察来自「各自有序」:任何时刻,所有还没被接走的节点里,全局最小的那个必然是两条链当前头节点之一。因为每条链内部越往后值越大,头就是各自的最小者,两个最小者再比一次,赢家就是全局最小。
把这个赢家接到结果尾部之后,剩下的局面仍然是「两条升序链表」——和原问题一模一样,只是规模少了一个节点。于是同一个判断可以一直重复下去,这正是归并排序里合并两个有序序列的经典操作。每一步都取当前能取的最小值,接出来的链自然从小到大,正确性不需要任何回头检查。
哑结点解决了什么问题
结果链表的头节点是谁,要等第一次比较之后才知道,可能来自任何一条链。不用哑结点的话,就得先单独比较一次、处理各种空链特判,才能把头定下来,代码立刻多出好几个分支。
哑结点(dummy)的做法是先放一个不存数据的占位节点,让尾指针 tail 从它出发,接第一个节点和接后面任何节点的动作完全一样:tail.next 指向被选中的节点,tail 前移。最后返回 dummy.next,占位节点自动被跳过。一个空节点换掉所有头部特判,这就是链表题里 dummy 反复出现的原因。
一条链走空后为什么能整段直接接上
循环条件是两条链都非空,所以必有一条先耗尽。此时剩下那条链上的每个节点都在之前的比较中「输」给过已接走的节点,也就是不小于结果链的最后一个值;而它内部本来就有序。两个条件合起来,整段原样接到尾部就是正确的升序,不需要再逐个比较。
这一步对应代码里的 tail.next = l1 if l1 else l2,忘写它是这道题最高频的 bug:一条链空了循环退出,另一条还挂着一整段没接,结果直接少一半数据。
复杂度与两个细节
时间 O(m+n):每个节点恰好被比较有限次、接入一次,两条链各扫一遍。空间 O(1):只用 dummy 和 tail 两个额外指针,节点全部复用。
细节一:两头相等时固定取第一条链的节点,保证排序稳定,也省一次多余判断。细节二:这套归并是合并 K 个升序链表(LeetCode 23)的基础——K 条链两两分治合并,或用小根堆每次弹出最小头,整体 O(N log K),其中每一次基本合并用的都是本题的写法。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这条「比头·取小·前移」,下面每一步都在套它。
- 4A、B 两条已排好。结果行先空着(? 是占位,接入后填真值)。p1 指 A 头 1,p2 指 B 头 2。
- 5比两个头:A 头 1 vs B 头 2。A 的 1 更小(或相等优先取 A),取它。
- 6把 1 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 3。
- 7比两个头:A 头 3 vs B 头 2。B 的 2 更小,取它。
- 8把 2 接到结果尾(变绿),从 B 取走的节点标灰。B 的指针前移到 4。
- 9比两个头:A 头 3 vs B 头 4。A 的 3 更小(或相等优先取 A),取它。
- 10把 3 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 5。
- 11比两个头:A 头 5 vs B 头 4。B 的 4 更小,取它。
- 12把 4 接到结果尾(变绿),从 B 取走的节点标灰。B 的指针前移到 6。
- 13比两个头:A 头 5 vs B 头 6。A 的 5 更小(或相等优先取 A),取它。
- 14把 5 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 7。
- 15比两个头:A 头 7 vs B 头 6。B 的 6 更小,取它。
- 16把 6 接到结果尾(变绿),从 B 取走的节点标灰。B 的指针前移到 末尾(空)。
- 17B 已经走空,剩下的 A 整段都比已接入的大,直接逐个接上。当前接 7。
- 18把 7 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 末尾(空)。
- 19两条都接完,结果 1→2→3→4→5→6→7。回头从头核一遍:第 0 个 1 ≤ 下一个 2。
- 20结果第 1 个 2 ≤ 下一个 3,升序成立。
- 21结果第 2 个 3 ≤ 下一个 4,升序成立。
- 22结果第 3 个 4 ≤ 下一个 5,升序成立。
- 23结果第 4 个 5 ≤ 下一个 6,升序成立。
- 24结果第 5 个 6 ≤ 下一个 7,升序成立。
- 25走到末尾 7,全程无降序,合并正确。双指针归并一遍搞定。
⚠️ 容易写错的地方
✗ 错:不用哑结点、单独判空头
✓ 对:哑结点统一处理
省去「结果头是谁」的特判,代码更短更稳
✗ 错:循环后忘接剩余
✓ 对:tail.next = 剩下那条
一条走空时另一条还挂着一整段
✗ 错:相等时随便取导致不稳定
✓ 对:相等优先取 l1
保持稳定、避免重复判断
完整代码(Python / C++ / Java)
Python
def mergeTwoLists(l1, l2):
dummy = tail = ListNode() # 哑结点 + 尾指针
while l1 and l2:
if l1.val <= l2.val:
tail.next = l1; l1 = l1.next
else:
tail.next = l2; l2 = l2.next
tail = tail.next
tail.next = l1 if l1 else l2 # 接上剩余
return dummy.nextC++
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2){
ListNode dummy, *tail = &dummy;
while(l1 && l2){
if(l1->val <= l2->val){ tail->next = l1; l1 = l1->next; }
else { tail->next = l2; l2 = l2->next; }
tail = tail->next;
}
tail->next = l1 ? l1 : l2;
return dummy.next;
}Java
public ListNode mergeTwoLists(ListNode l1, ListNode l2){
ListNode dummy = new ListNode(0), tail = dummy;
while(l1 != null && l2 != null){
if(l1.val <= l2.val){ tail.next = l1; l1 = l1.next; }
else { tail.next = l2; l2 = l2.next; }
tail = tail.next;
}
tail.next = (l1 != null) ? l1 : l2;
return dummy.next;
}复杂度
时间
O(m+n)
每个节点恰好接一次
空间
O(1)
只接原节点,不新建
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 合并两个有序链表 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
合并 K 条升序链表怎么做?+
LC23:两两归并(分治)或用小根堆每次取最小头,复杂度 O(N log K)。
为什么用哑结点更好?+
结果头节点在第一次比较前还不知道是谁,哑结点让 tail 一开始就有落脚点,省掉对头节点的特判。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 合并两个有序链表 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。