力扣解题-21. 合并两个有序链表

34 阅读7分钟

力扣解题-21. 合并两个有序链表

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

示例 1:

image.png

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

输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []

输出:[]

示例 3:

输入:l1 = [], l2 = [0]

输出:[0]

提示:

两个链表的节点数目范围是 [0, 50]

-100 <= Node.val <= 100

l1 和 l2 均按 非递减顺序 排列

Related Topics

递归、链表


第一次解答

解题思路

核心方法:迭代法(新建节点版),通过哑节点(dummyHead)构建新链表,逐位比较两个有序链表的当前节点值,选择较小/相等的值创建新节点加入结果链表,遍历完成后拼接剩余节点,逻辑直观但会创建新节点(额外空间开销)。

核心逻辑拆解

合并两个升序链表的核心是“双指针逐位比较,按序拼接”:

  1. 边界处理:若其中一个链表为空,直接返回另一个链表(空链表与任何链表合并结果都是另一个链表);
  2. 哑节点初始化:创建dummyHead(哑节点)作为结果链表的虚拟头节点,current指针指向哑节点,用于构建结果链表;
  3. 双指针遍历比较
    • 循环条件:list1 != null && list2 != null(两个链表都有未处理节点);
    • 取当前节点值val1(list1)和val2(list2);
    • 分三种情况处理:
      • val1 < val2:创建val1的新节点,current.next指向该节点,list1后移,current后移;
      • val1 == val2:依次创建val1val2的新节点,两个链表均后移,current移动两步;
      • val1 > val2:创建val2的新节点,current.next指向该节点,list2后移,current后移;
  4. 拼接剩余节点
    • 若list1为空,current.next直接指向list2的剩余节点;
    • 若list2为空,current.next直接指向list1的剩余节点;
  5. 返回结果:返回dummyHead.next(跳过哑节点,得到结果链表的真实头节点)。
具体步骤(以示例1 list1=[1,2,4]、list2=[1,3,4]为例)
步骤list1值list2值操作结果链表(current指向)
111创建1、1节点,list1/list2后移dummyHead→1→1
223创建2节点,list1后移1→1→2
343创建3节点,list2后移1→1→2→3
444创建4、4节点,list1/list2后移1→1→2→3→4→4
5nullnull循环终止-
最终结果链表:1→1→2→3→4→4,与示例一致。
性能说明
  • 时间复杂度:O(n+m)(n、m分别为两个链表的长度,每个节点仅被访问一次);
  • 空间复杂度:O(n+m)(创建了n+m个新节点存储结果,额外空间开销);
  • 优势:
    1. 逻辑简单易懂,新手易上手;
    2. 不修改原链表节点,仅创建新节点,无数据副作用;
  • 可优化点:
    1. 相等值的处理可简化(无需分三个if,合并为<=判断);
    2. 无需创建新节点,可直接复用原链表节点(空间复杂度优化为O(1))。
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
         if(list1==null){
             return list2;
         }
         if(list2==null){
             return list1;
         }
         ListNode dummyHead = new ListNode(0);
         ListNode current=dummyHead;
         while(list1!=null&&list2!=null){
             int val1=list1.val;
             int val2=list2.val;
             if(val1<val2){
                 current.next=new ListNode(val1);
                 list1=list1.next;
                 current=current.next;
             }
             if(val1==val2){
                 current.next=new ListNode(val1);
                 current.next.next=new ListNode(val2);
                 current=current.next.next;
                 list1=list1.next;
                 list2=list2.next;
             }
             if(val1>val2){
                 current.next=new ListNode(val2);
                 list2=list2.next;
                 current=current.next;
             }
         }
         if(list1==null){
             current.next=list2;
         }
         if(list2==null){
             current.next=list1;
         }
         return dummyHead.next;
     }

示例解答

解题思路

解法1:递归法(最优解,复用原节点)

核心方法:递归合并(分治思想),通过递归比较两个链表的当前节点值,将较小值的节点作为当前结果节点,其next指向剩余链表的合并结果,无需创建新节点,空间复杂度O(1)(递归栈除外),代码极简且效率高。

核心原理铺垫(递归的合理性)

合并两个升序链表可拆解为“选择当前最小节点 + 合并剩余链表”的子问题:

  • l1.val < l2.val:结果为l1 + 合并l1.nextl2的结果;
  • l1.val ≥ l2.val:结果为l2 + 合并l1l2.next的结果;
  • 递归终止条件:其中一个链表为空,返回另一个链表。
核心逻辑拆解
  1. 递归终止条件
    • l1 == null,返回l2
    • l2 == null,返回l1
  2. 递归选择节点
    • l1.val < l2.val
      • l1.next = 递归合并l1.nextl2的结果;
      • 返回l1(当前最小节点);
    • 否则:
      • l2.next = 递归合并l1l2.next的结果;
      • 返回l2(当前最小节点);
  3. 递归过程:每一层递归仅选择当前最小节点,剩余部分交给下一层递归处理,最终拼接成完整的升序链表。
具体步骤(以示例1 l1=[1,2,4]、l2=[1,3,4]为例)
  1. 第一层递归:l1.val=1,l2.val=1 → 进入else分支,l2.next = merge(l1, l2.next=[3,4]);
  2. 第二层递归:l1.val=1,l2.val=3 → l1.next = merge(l1.next=[2,4], l2=[3,4]),返回l1=1;
  3. 第三层递归:l1.val=2,l2.val=3 → l1.next = merge(l1.next=[4], l2=[3,4]),返回l1=2;
  4. 第四层递归:l1.val=4,l2.val=3 → l2.next = merge(l1=[4], l2.next=[4]),返回l2=3;
  5. 第五层递归:l1.val=4,l2.val=4 → l2.next = merge(l1=[4], l2.next=null),返回l2=4;
  6. 第六层递归:l1=[4],l2=null → 返回l1=4;
  7. 递归回溯拼接:最终得到1→1→2→3→4→4。
性能说明
  • 时间复杂度:O(n+m)(每个节点仅被访问一次);
  • 空间复杂度:O(n+m)(递归调用栈的深度,最坏情况为n+m层,如一个链表所有节点都小于另一个);
  • 核心优势:
    1. 代码极简,仅几行逻辑完成合并;
    2. 复用原链表节点,无额外节点创建开销;
    3. 符合分治思想,易于理解递归的核心逻辑。
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        } else if (l2 == null) {
            return l1;
        } else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
解法2:迭代优化版(复用原节点,O(1)额外空间)

核心方法:迭代法(复用原节点),在第一次解答的迭代思路基础上,直接复用原链表节点(不创建新节点),仅调整节点的next指针,额外空间复杂度优化为O(1),是工程中更优的迭代实现。

代码实现
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    ListNode dummy = new ListNode(0);
    ListNode curr = dummy;
    
    // 双指针遍历,复用原节点
    while (list1 != null && list2 != null) {
        if (list1.val <= list2.val) {
            curr.next = list1;
            list1 = list1.next;
        } else {
            curr.next = list2;
            list2 = list2.next;
        }
        curr = curr.next;
    }
    
    // 拼接剩余节点
    curr.next = list1 != null ? list1 : list2;
    
    return dummy.next;
}
优势说明
  • 时间复杂度:O(n+m),与原迭代版一致;
  • 额外空间复杂度:O(1)(仅使用哑节点和指针,无新节点创建);
  • 核心优化点:
    1. 合并val1 < val2val1 == val2的判断为<=,简化逻辑;
    2. 直接将原节点接入结果链表,避免新节点创建的内存开销;
    3. 代码更简洁,执行效率更高。

总结

  1. 迭代新建节点版(第一次解答):逻辑直观,O(n+m)时间+O(n+m)空间,适合理解核心思路但有额外空间开销;
  2. 递归法(示例解答):代码极简,O(n+m)时间+O(n+m)空间(递归栈),复用原节点,适合偏好递归风格的场景;
  3. 迭代复用节点版(最优解):O(n+m)时间+O(1)额外空间,复用原节点+简化逻辑,工程首选;
  4. 关键技巧:
    • 核心思想:双指针逐位比较,按升序拼接,分治/迭代均可实现;
    • 空间优化:优先复用原链表节点,避免创建新节点;
    • 边界处理:空链表直接返回另一个链表,遍历完成后拼接剩余节点。