力扣解题-21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入: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)构建新链表,逐位比较两个有序链表的当前节点值,选择较小/相等的值创建新节点加入结果链表,遍历完成后拼接剩余节点,逻辑直观但会创建新节点(额外空间开销)。
核心逻辑拆解
合并两个升序链表的核心是“双指针逐位比较,按序拼接”:
- 边界处理:若其中一个链表为空,直接返回另一个链表(空链表与任何链表合并结果都是另一个链表);
- 哑节点初始化:创建
dummyHead(哑节点)作为结果链表的虚拟头节点,current指针指向哑节点,用于构建结果链表; - 双指针遍历比较:
- 循环条件:
list1 != null && list2 != null(两个链表都有未处理节点); - 取当前节点值
val1(list1)和val2(list2); - 分三种情况处理:
val1 < val2:创建val1的新节点,current.next指向该节点,list1后移,current后移;val1 == val2:依次创建val1、val2的新节点,两个链表均后移,current移动两步;val1 > val2:创建val2的新节点,current.next指向该节点,list2后移,current后移;
- 循环条件:
- 拼接剩余节点:
- 若list1为空,
current.next直接指向list2的剩余节点; - 若list2为空,
current.next直接指向list1的剩余节点;
- 若list1为空,
- 返回结果:返回
dummyHead.next(跳过哑节点,得到结果链表的真实头节点)。
具体步骤(以示例1 list1=[1,2,4]、list2=[1,3,4]为例)
| 步骤 | list1值 | list2值 | 操作 | 结果链表(current指向) |
|---|---|---|---|---|
| 1 | 1 | 1 | 创建1、1节点,list1/list2后移 | dummyHead→1→1 |
| 2 | 2 | 3 | 创建2节点,list1后移 | 1→1→2 |
| 3 | 4 | 3 | 创建3节点,list2后移 | 1→1→2→3 |
| 4 | 4 | 4 | 创建4、4节点,list1/list2后移 | 1→1→2→3→4→4 |
| 5 | null | null | 循环终止 | - |
| 最终结果链表:1→1→2→3→4→4,与示例一致。 |
性能说明
- 时间复杂度:O(n+m)(n、m分别为两个链表的长度,每个节点仅被访问一次);
- 空间复杂度:O(n+m)(创建了n+m个新节点存储结果,额外空间开销);
- 优势:
- 逻辑简单易懂,新手易上手;
- 不修改原链表节点,仅创建新节点,无数据副作用;
- 可优化点:
- 相等值的处理可简化(无需分三个if,合并为
<=判断); - 无需创建新节点,可直接复用原链表节点(空间复杂度优化为O(1))。
- 相等值的处理可简化(无需分三个if,合并为
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.next和l2的结果; - 若
l1.val ≥ l2.val:结果为l2+ 合并l1和l2.next的结果; - 递归终止条件:其中一个链表为空,返回另一个链表。
核心逻辑拆解
- 递归终止条件:
- 若
l1 == null,返回l2; - 若
l2 == null,返回l1;
- 若
- 递归选择节点:
- 若
l1.val < l2.val:l1.next= 递归合并l1.next和l2的结果;- 返回
l1(当前最小节点);
- 否则:
l2.next= 递归合并l1和l2.next的结果;- 返回
l2(当前最小节点);
- 若
- 递归过程:每一层递归仅选择当前最小节点,剩余部分交给下一层递归处理,最终拼接成完整的升序链表。
具体步骤(以示例1 l1=[1,2,4]、l2=[1,3,4]为例)
- 第一层递归:l1.val=1,l2.val=1 → 进入else分支,l2.next = merge(l1, l2.next=[3,4]);
- 第二层递归:l1.val=1,l2.val=3 → l1.next = merge(l1.next=[2,4], l2=[3,4]),返回l1=1;
- 第三层递归:l1.val=2,l2.val=3 → l1.next = merge(l1.next=[4], l2=[3,4]),返回l1=2;
- 第四层递归:l1.val=4,l2.val=3 → l2.next = merge(l1=[4], l2.next=[4]),返回l2=3;
- 第五层递归:l1.val=4,l2.val=4 → l2.next = merge(l1=[4], l2.next=null),返回l2=4;
- 第六层递归:l1=[4],l2=null → 返回l1=4;
- 递归回溯拼接:最终得到1→1→2→3→4→4。
性能说明
- 时间复杂度:O(n+m)(每个节点仅被访问一次);
- 空间复杂度:O(n+m)(递归调用栈的深度,最坏情况为n+m层,如一个链表所有节点都小于另一个);
- 核心优势:
- 代码极简,仅几行逻辑完成合并;
- 复用原链表节点,无额外节点创建开销;
- 符合分治思想,易于理解递归的核心逻辑。
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)(仅使用哑节点和指针,无新节点创建);
- 核心优化点:
- 合并
val1 < val2和val1 == val2的判断为<=,简化逻辑; - 直接将原节点接入结果链表,避免新节点创建的内存开销;
- 代码更简洁,执行效率更高。
- 合并
总结
- 迭代新建节点版(第一次解答):逻辑直观,O(n+m)时间+O(n+m)空间,适合理解核心思路但有额外空间开销;
- 递归法(示例解答):代码极简,O(n+m)时间+O(n+m)空间(递归栈),复用原节点,适合偏好递归风格的场景;
- 迭代复用节点版(最优解):O(n+m)时间+O(1)额外空间,复用原节点+简化逻辑,工程首选;
- 关键技巧:
- 核心思想:双指针逐位比较,按升序拼接,分治/迭代均可实现;
- 空间优化:优先复用原链表节点,避免创建新节点;
- 边界处理:空链表直接返回另一个链表,遍历完成后拼接剩余节点。