21. 合并两个有序链表(Merge Two Sorted Lists)

难度:简单 | 主题:链表、递归

题目

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例

 
输入:l1 = [1,2,4], l2 = [1,3,4]
 
输出:[1,1,2,3,4,4]
 

思路

先讲个故事:两支有序的队伍合并

两队小朋友已经按身高从矮到高排好了。老师想把他们合并成一队,还是按身高从矮到高。

老师从两队队首各拉一个小朋友出来比较——矮的进队,高的回去等下一轮。哪队先没人了,另一队剩下的人直接排上去。


引导式推导:从合并到递归

第 1 层:迭代(哨兵节点 + 双指针)

两个链表各一指针,比较当前节点值,小的接到结果链表尾部。

 
l1: [1, 2, 4]
 
l2: [1, 3, 4]
 
比较 1 vs 1 → 取 l2 的 1(稳定排序)
 
比较 1 vs 3 → 取 l1 的 1
 
比较 2 vs 3 → 取 l1 的 2
 
比较 4 vs 3 → 取 l2 的 3
 
比较 4 vs 4 → 取 l1 的 4
 
剩下 l2 的 4 → 直接接上
 
结果:[1, 1, 2, 3, 4, 4]
 

为什么用哨兵节点(dummy)?

避免单独处理头节点——如果不确定第一个节点来自 l1 还是 l2,dummy 让你统一用 tail.next = ... 追加。

第 2 层:递归

 
merge([1,2,4], [1,3,4])
 
= 1→merge([2,4], [1,3,4])     // 1 vs 1, 取 l1 的 1
 
= 1→1→merge([2,4], [3,4])     // 2 vs 1, 取 l2 的 1
 
= 1→1→2→merge([4], [3,4])     // 2 vs 3, 取 l1 的 2
 
= 1→1→2→3→merge([4], [4])     // 4 vs 3, 取 l2 的 3
 
= 1→1→2→3→4→merge([], [4])    // 4 vs 4, 取 l1 的 4
 
= 1→1→2→3→4→4                 // l1 空了,返回 l2
 

graph LR

    subgraph 合并过程

        A["L1: ①→②→④<br/>L2: ①→③→④"] --> B["比较: ① vs ①"]

        B --> C["取 L2 的 ①"]

        C --> D["比较: ① vs ③"]

        D --> E["取 L1 的 ①"]

        E --> F["比较: ② vs ③"]

        F --> G["..."]

        G --> H["结果: ①→①→②→③→④→④"]

    end

    style H fill:#ffd700

解法时间空间
迭代O(n+m)O(1)
递归O(n+m)O(n+m)

代码

 
# 迭代(推荐)
 
def mergeTwoLists(self, list1, list2):
 
    dummy = tail = ListNode()
 
    while list1 and list2:
 
        if list1.val <= list2.val:
 
            tail.next = list1
 
            list1 = list1.next
 
        else:
 
            tail.next = list2
 
            list2 = list2.next
 
        tail = tail.next
 
    tail.next = list1 or list2
 
    return dummy.next
 
# 递归
 
def mergeTwoLists(self, list1, list2):
 
    if not list1:
 
        return list2
 
    if not list2:
 
        return list1
 
    if list1.val <= list2.val:
 
        list1.next = self.mergeTwoLists(list1.next, list2)
 
        return list1
 
    list2.next = self.mergeTwoLists(list1, list2.next)
 
    return list2
 

复杂度

解法时间空间
迭代O(n+m)O(1)
递归O(n+m)O(n+m)

实战考量

频率分析

出现在:链表入门”必做题”,约 50% 的 AI Agent 会作为热身或归并排序的子问题。不会这题后面没法做排序链表(148 题)。

延伸思考

Q:合并 k 个有序链表怎么做?

A:用最小堆(优先队列),k 个头入堆,弹出最小后把它的 next 入堆,重复到空。O(N log k)。

Q:合并两个无序链表并去重呢?

A:先各自排序(归并排序),再调用本方法合并,合并时跳过重复值。

Q:递归版空间复杂度为什么是 O(n+m)?

A:递归层数等于合并后链表长度,每层需要栈空间。深度跟输入总长度成正比。

易错点

  • tail = tail.next 必须在循环内维护,不能忘

  • 接剩余部分用 tail.next = list1 or list2 最简洁

  • 返回 dummy.next,不是 dummy


生活类比

合并有序链表 → 两队小朋友排队

哑节点就像在最前面举着的”虚拟小旗”,让你永远不需要问”谁是第一个”。从两队队首各拉一个比较,矮的走,高的等。一队没人了,另一队剩下的直接排上。


相关题目

题目关系
88合并两个有序数组合并模式同族(数组版)
23合并K个升序链表K 个链表合并(堆/分治)
146LRU缓存哨兵节点技巧同源

→ 返回题单:LeetCode学习路线图 > 四、链表

速记卡(面试闪卡)

Q1:一句话讲清「21. 合并两个有序链表(Merge Two Sorted Lists)」到底是什么?

A:将两个升序链表合并成一个新的升序链表,迭代 O(n+m) 时间 O(1) 空间。

Q2:思路 —— 怎么理解?

A:像两队按身高排队合并——各拉队首比,矮的进队、高的等,一队空了另一队直接接上。迭代用哨兵节点(dummy)省去头节点特判;也可递归实现。

Q3:代码 —— 怎么理解?

A:dummy=tail=ListNode(),循环比较两指针小的接到 tail、tail 后移;循环完 tail.next=list1 or list2 接剩余;返回 dummy.next。递归版:空返回另一链表,否则小者.next=递归(剩余)。

Q4:复杂度 —— 怎么理解?

A:迭代时间 O(n+m) 空间 O(1);递归时间 O(n+m) 空间 O(n+m)(递归栈深度等于总长)。

Q5:实战考量 —— 怎么理解?

A:链表入门必做,约 50% 出现,是归并排序/148 题的基础;合并 k 个用最小堆 O(N log k);易错:tail 后移别忘、返回 dummy.next 而非 dummy。

Q6:核心速记主线有哪些?

  • 双指针比较队首,小的接结果,哨兵 dummy 省头节点特判

  • 一队空了直接接另一队剩余

  • 迭代 O(n+m)/O(1),递归同样时间但 O(n+m) 栈

  • 易错:tail 后移、返回 dummy.next、合并 k 个用堆

口诀

A:两有序链表合并,双指针比队首

矮的先接哨兵后,一队空接另队尾

迭代 O(n+m) 空间一,递归栈深要留意

dummy 举旗免特判,合并 k 用最小堆

相关链接