题目描述
思路解析
一句话答案: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),其中每一次基本合并用的都是本题的写法。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条「比头·取小·前移」,下面每一步都在套它。
A、B 两条已排好。结果行先空着(? 是占位,接入后填真值)。p1 指 A 头 1,p2 指 B 头 2。
比两个头:A 头 1 vs B 头 2。A 的 1 更小(或相等优先取 A),取它。
把 1 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 3。
比两个头:A 头 3 vs B 头 2。B 的 2 更小,取它。
把 2 接到结果尾(变绿),从 B 取走的节点标灰。B 的指针前移到 4。
比两个头:A 头 3 vs B 头 4。A 的 3 更小(或相等优先取 A),取它。
把 3 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 5。
比两个头:A 头 5 vs B 头 4。B 的 4 更小,取它。
把 4 接到结果尾(变绿),从 B 取走的节点标灰。B 的指针前移到 6。
比两个头:A 头 5 vs B 头 6。A 的 5 更小(或相等优先取 A),取它。
把 5 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 7。
比两个头:A 头 7 vs B 头 6。B 的 6 更小,取它。
把 6 接到结果尾(变绿),从 B 取走的节点标灰。B 的指针前移到 末尾(空)。
B 已经走空,剩下的 A 整段都比已接入的大,直接逐个接上。当前接 7。
把 7 接到结果尾(变绿),从 A 取走的节点标灰。A 的指针前移到 末尾(空)。
两条都接完,结果 1→2→3→4→5→6→7。回头从头核一遍:第 0 个 1 ≤ 下一个 2。
结果第 1 个 2 ≤ 下一个 3,升序成立。
结果第 2 个 3 ≤ 下一个 4,升序成立。
结果第 3 个 4 ≤ 下一个 5,升序成立。
结果第 4 个 5 ≤ 下一个 6,升序成立。
结果第 5 个 6 ≤ 下一个 7,升序成立。
走到末尾 7,全程无降序,合并正确。双指针归并一遍搞定。
空链表与单节点边界先想清。
两个高频追问。
参考代码
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.next复杂度
- 时间:O(m+n),每个节点恰好接一次
- 空间:O(1),只接原节点,不新建
易错点
面试追问把动画讲成自己的话
追问合并 K 条升序链表怎么做?
追问为什么用哑结点更好?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
重排链表
LeetCode 143 · 中等 · 沿着 链表 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题