【Hot100|26-LeetCode 21. 合并两个有序链表 - 完整解法详解】


一、问题理解

问题描述

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

示例

示例 1:

text

复制代码
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

text

复制代码
输入:l1 = [], l2 = []
输出:[]

示例 3:

text

复制代码
输入:l1 = [], l2 = [0]
输出:[0]

要求

  • 时间复杂度: O(n + m),其中 n 和 m 分别为两个链表的长度。

  • 空间复杂度: 迭代法 O(1),递归法 O(n + m)(递归调用栈空间)。

  • 新链表通过拼接原节点得到,不创建新节点。


二、核心思路:比较与连接

基本思想

合并两个有序链表与合并两个有序数组类似,核心是不断比较两个链表当前节点的值,将较小值的节点接到结果链表的末尾,直到其中一个链表遍历完,然后将另一个链表的剩余部分直接接上。

有两种主流实现方式:

  1. 迭代法:使用一个哨兵节点(dummy)简化边界处理,通过循环逐步连接节点。

  2. 递归法:每次比较两个头节点,将较小值的节点作为结果头,然后递归地合并剩余部分。


三、代码逐行解析

方法一:迭代法(最优解,O(1) 空间)

核心思想
  • 创建一个虚拟头节点 dummy,用于简化头节点的处理。

  • 使用指针 prev 指向当前结果链表的末尾。

  • 循环比较 l1l2 当前节点的值,将较小者接到 prev 后面,并移动相应指针。

  • 循环结束后,将非空链表的剩余部分直接接到 prev 后面。

  • 返回 dummy.next 作为合并后的头节点。

Python 解法

python

复制代码
# Definition for singly-linked list.
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        # 创建一个虚拟头节点,它的next最终指向合并后的链表头
        dummy = ListNode()
        prev = dummy  # prev 始终指向结果链表的最后一个节点

        # 当两个链表都不为空时,比较并连接
        while l1 and l2:
            if l1.val <= l2.val:
                prev.next = l1
                l1 = l1.next
            else:
                prev.next = l2
                l2 = l2.next
            prev = prev.next  # prev 向后移动

        # 连接剩余部分(l1 或 l2 可能还有剩余)
        prev.next = l1 if l1 else l2

        return dummy.next
Java 解法

java

复制代码
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 创建虚拟头节点
        ListNode dummy = new ListNode();
        ListNode prev = dummy;  // prev 指向结果链表的尾节点

        // 当两个链表都不为空时
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }

        // 连接剩余部分
        prev.next = (l1 != null) ? l1 : l2;

        return dummy.next;
    }
}

方法二:递归法(代码简洁,但空间复杂度较高)

核心思想
  • 递归函数 mergeTwoLists 接收两个链表头 l1l2,返回合并后的头节点。

  • 基准条件:如果其中一个链表为空,直接返回另一个链表。

  • 比较两个头节点的值,将较小值的节点作为合并后的头节点,然后将其 next 指向递归合并剩余部分的结果。

Python 解法

python

复制代码
class Solution:
    def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
        # 基准条件:如果有一个链表为空,返回另一个
        if not l1:
            return l2
        if not l2:
            return l1

        # 比较头节点值,较小者作为头,并递归合并剩余部分
        if l1.val <= l2.val:
            l1.next = self.mergeTwoLists(l1.next, l2)
            return l1
        else:
            l2.next = self.mergeTwoLists(l1, l2.next)
            return l2
Java 解法

java

复制代码
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 基准条件
        if (l1 == null) return l2;
        if (l2 == null) return l1;

        // 比较头节点值
        if (l1.val <= l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

四、Java 与 Python 语法对比

操作 Java Python
链表节点定义 public class ListNode { int val; ListNode next; ListNode() {} ... } class ListNode: def __init__(self, val=0, next=None):
创建节点 new ListNode(val) ListNode(val)
空值判断 node == null node is None
条件判断 if (l1 != null && l2 != null) if l1 and l2:
三目运算符 (l1 != null) ? l1 : l2 l1 if l1 else l2(或 l1 or l2
递归调用 mergeTwoLists(l1.next, l2) self.mergeTwoLists(l1.next, l2)

五、实例演示

示例:l1 = [1,2,4]l2 = [1,3,4]

迭代法过程
步骤 l1 当前 l2 当前 比较 操作 结果链表(prev 指向末尾)
初始 1 1 - dummy -> null dummy
1 1 1 1 <= 1 接 l1 的 1 dummy -> 1
2 2 1 1 < 2 接 l2 的 1 dummy -> 1 -> 1
3 2 3 2 < 3 接 l1 的 2 dummy -> 1 -> 1 -> 2
4 4 3 3 < 4 接 l2 的 3 dummy -> 1 -> 1 -> 2 -> 3
5 4 4 4 <= 4 接 l1 的 4 dummy -> 1 -> 1 -> 2 -> 3 -> 4
6 null 4 l1 空 接剩余 l2 的 4 dummy -> 1 -> 1 -> 2 -> 3 -> 4 -> 4

最终结果:[1,1,2,3,4,4]

递归法过程

递归调用栈:

text

复制代码
merge(1,1) → 比较 1 和 1,取 l1 的 1,然后 merge(2,1)
  → merge(2,1) → 比较 2 和 1,取 l2 的 1,然后 merge(2,3)
    → merge(2,3) → 比较 2 和 3,取 l1 的 2,然后 merge(4,3)
      → merge(4,3) → 比较 4 和 3,取 l2 的 3,然后 merge(4,4)
        → merge(4,4) → 比较 4 和 4,取 l1 的 4,然后 merge(null,4)
          → merge(null,4) → 返回 4
        → 返回 4 → 4.next = 4,返回 4
      → 返回 3 → 3.next = 4,返回 3
    → 返回 2 → 2.next = 3,返回 2
  → 返回 1 → 1.next = 2,返回 1
→ 返回 1 → 1.next = 1,返回 1

最终得到链表:1 → 1 → 2 → 3 → 4 → 4 → null


六、关键细节解析

1. 为什么迭代法需要 dummy 节点?

如果没有 dummy 节点,我们需要单独处理合并后头节点为空的情况(即第一个节点谁来接)。dummy 节点作为哨兵,使得所有节点的操作统一,最后返回 dummy.next 即可,避免了对头节点的特殊处理。

2. 迭代法中,为什么最后可以直接 prev.next = l1 if l1 else l2

因为 l1 和 l2 中至少有一个为 null,而另一个可能还有剩余节点。由于原链表已经有序,剩余部分整体大于等于已合并部分,直接接上即可。

3. 递归法的基准条件为什么是 if not l1: return l2if not l2: return l1

这处理了空链表的情况。如果其中一个链表为空,合并结果就是另一个链表。这也是递归的终止条件,避免了无限递归。

4. 递归法中,如何保证不丢失节点?

递归调用 mergeTwoLists(l1.next, l2) 会返回合并后的链表头,将其赋值给 l1.next,这样 l1 就连接上了后续合并的结果。由于 l1 本身被返回作为当前层的结果头,整个链表的连接是完整的。

5. 如何处理两个链表都为空的情况?

迭代法中,while 循环不执行,prev.next = l1 if l1 else l2 中 l1 和 l2 均为 null,所以 prev.next = null,最终返回 dummy.next 也为 null,正确。递归法中,第一个基准条件会触发,例如 if not l1: return l2,此时 l1 为 null,l2 也为 null,返回 null。


七、复杂度分析

迭代法

  • 时间复杂度: O(n + m),其中 n 和 m 分别为两个链表的长度。每个节点被访问一次。

  • 空间复杂度: O(1),只使用了常数个指针变量。

递归法

  • 时间复杂度: O(n + m),每个节点被递归调用一次。

  • 空间复杂度: O(n + m),递归调用栈的深度等于合并后链表的长度,在最坏情况下为 n + m。


八、其他解法

解法三:原地合并(不使用 dummy,单独处理头节点)

可以不用 dummy 节点,但需要先确定合并后的头节点,然后循环处理后续节点。代码稍显繁琐,但原理相同。

python

复制代码
def mergeTwoLists(self, l1, l2):
    if not l1 or not l2:
        return l1 or l2
    
    # 确定头节点
    if l1.val <= l2.val:
        head = l1
        l1 = l1.next
    else:
        head = l2
        l2 = l2.next
    
    curr = head
    while l1 and l2:
        if l1.val <= l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    
    curr.next = l1 or l2
    return head

九、常见问题与解答

Q1: 迭代法中,为什么使用 dummy 而不是直接操作 head

A1: dummy 简化了头节点的处理。如果不使用 dummy,需要先判断哪个链表的头节点更小,作为结果头节点,然后在循环中处理,代码会多出一些条件分支。dummy 让所有节点的操作一致,代码更简洁。

Q2: 递归法会不会导致栈溢出?

A2: 如果链表非常长(例如上万节点),递归深度可能过大,导致栈溢出。在实际应用中,迭代法更安全。但 LeetCode 上通常不会出现这种情况。

Q3: 如果两个链表中有重复值,如何保证稳定性?

A3: 题目没有要求稳定性(即相等元素的相对顺序)。但我们的代码中,当 l1.val <= l2.val 时选择 l1,这保持了 l1 中相等元素在前,但通常合并两个有序链表不需要考虑稳定性。

Q4: 能否修改原链表的结构?

A4: 题目允许通过拼接原节点来创建新链表,因此可以修改原链表的 next 指针,但不创建新节点。

Q5: 如何测试代码?

A5: 可以测试以下情况:

  • 两个链表都为空

  • 一个为空,另一个非空

  • 两个长度相等,值交错

  • 一个链表的所有值都小于另一个

  • 包含重复值


十、相关题目

1. LeetCode 23. 合并K个升序链表

题目: 合并 k 个有序链表。可以使用分治法或优先队列,本题是它的基础。

2. LeetCode 88. 合并两个有序数组

题目: 合并两个有序数组到第一个数组中。思路类似,但需要从后往前合并以避免覆盖。

3. LeetCode 148. 排序链表

题目: 对链表进行排序,可以用归并排序,其中合并两个有序链表是核心步骤。

4. LeetCode 2. 两数相加

题目: 两个链表表示的数字相加,也涉及链表遍历和连接。


十一、总结

核心要点

  • 问题本质: 将两个有序链表合并成一个有序链表,不创建新节点。

  • 两种主流解法:

    • 迭代法:使用 dummy 节点,时间复杂度 O(n+m),空间 O(1)。

    • 递归法:代码简洁,但空间复杂度 O(n+m)。

  • 关键操作: 比较节点值,将较小者接到结果链表末尾,移动指针。

算法步骤(迭代法)

  1. 创建 dummy 节点,prev 指向它。

  2. l1l2 都不为空时:

    • 比较 l1.vall2.val,将较小者接到 prev.next

    • 移动较小者所在链表的指针,以及 prev

  3. 将剩余的非空链表接到 prev.next

  4. 返回 dummy.next

复杂度对比

解法 时间复杂度 空间复杂度 优点 缺点
迭代法 O(n+m) O(1) 空间最优,无栈溢出风险 代码稍长
递归法 O(n+m) O(n+m) 代码简洁优雅 可能栈溢出

扩展思考

合并两个有序链表是许多复杂链表问题的基础,如归并排序、合并K个链表等。掌握迭代和递归两种写法,有助于灵活应对不同场景。特别是 dummy 节点的使用,是链表操作中简化边界处理的常用技巧。


相关推荐
张3蜂2 小时前
python知识点点亮
开发语言·python
Katecat996632 小时前
矿井地雷检测与识别:Yolo11-HAFB-1模型应用详解
python
星月总相伴2 小时前
python作用域
开发语言·python
阿里嘎多学长2 小时前
2026-02-15 GitHub 热点项目精选
开发语言·程序员·github·代码托管
维度攻城狮2 小时前
Python控制系统仿真案例-串联PID控制
python·simulink·pid·串级pid
嵌入式×边缘AI:打怪升级日志2 小时前
第十一章:主控访问多个传感器(Modbus 网关/桥接器设计)
开发语言·javascript·ecmascript
~央千澈~2 小时前
抖音弹幕游戏开发之第10集:整合 - 弹幕触发键盘操作·优雅草云桧·卓伊凡
开发语言·python·计算机外设
Laughtin2 小时前
macos的python安装选择以及homebrew python的安装方法
开发语言·python·macos
默凉2 小时前
C++ 编译过程
开发语言·c++