合并两个有序链表
问题简介
📝 题目描述
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
🧪 示例说明
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
💡 解题思路
✅ 方法一:递归法(推荐)
核心思想 :
比较两个链表头节点的值,较小者作为合并后链表的头节点,然后递归处理剩余部分。
步骤如下:
- 如果
l1为空,直接返回l2; - 如果
l2为空,直接返回l1; - 若
l1.val <= l2.val,则l1.next = mergeTwoLists(l1.next, l2); - 否则,
l2.next = mergeTwoLists(l1, l2.next); - 返回较小节点作为当前头节点。
优点:代码简洁,逻辑清晰。
缺点:递归深度可能较大(最坏 O(m+n)),在极端情况下可能导致栈溢出(但 LeetCode 测试用例通常不会触发)。
✅ 方法二:迭代法(双指针)
核心思想 :
使用一个虚拟头节点(dummy node)简化边界处理,用两个指针分别遍历两个链表,每次选择较小节点连接到结果链表。
步骤如下:
- 创建虚拟头节点
dummy和当前指针cur = dummy; - 当
l1和l2都非空时:- 若
l1.val <= l2.val,则cur.next = l1,l1 = l1.next; - 否则
cur.next = l2,l2 = l2.next; cur = cur.next;
- 若
- 循环结束后,将未遍历完的链表(
l1或l2)接到cur.next; - 返回
dummy.next。
优点:空间复杂度 O(1),无递归开销。
缺点:代码略长,需注意指针操作。
💻 代码实现
java:Java
// 方法一:递归
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null) return list2;
if (list2 == null) return list1;
if (list1.val <= list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
}
// 方法二:迭代(双指针 + 虚拟头节点)
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
// 接上剩余部分
cur.next = (list1 != null) ? list1 : list2;
return dummy.next;
}
}
go:Go
// 方法一:递归
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
if list1 == nil {
return list2
}
if list2 == nil {
return list1
}
if list1.Val <= list2.Val {
list1.Next = mergeTwoLists(list1.Next, list2)
return list1
} else {
list2.Next = mergeTwoLists(list1, list2.Next)
return list2
}
}
// 方法二:迭代
func mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode {
dummy := &ListNode{Val: -1}
cur := dummy
for list1 != nil && list2 != nil {
if list1.Val <= list2.Val {
cur.Next = list1
list1 = list1.Next
} else {
cur.Next = list2
list2 = list2.Next
}
cur = cur.Next
}
if list1 != nil {
cur.Next = list1
} else {
cur.Next = list2
}
return dummy.Next
}
🎯 示例演示(以示例1为例)
初始状态:
l1: 1 → 2 → 4
l2: 1 → 3 → 4
迭代过程(方法二):
| 步骤 | cur.next | l1 | l2 | 结果链表(dummy→...) |
|---|---|---|---|---|
| 0 | - | 1→2→4 | 1→3→4 | -1 |
| 1 | 1 (l2) | 1→2→4 | 3→4 | -1 → 1 |
| 2 | 1 (l1) | 2→4 | 3→4 | -1 → 1 → 1 |
| 3 | 2 | 4 | 3→4 | ... → 1 → 2 |
| 4 | 3 | 4 | 4 | ... → 2 → 3 |
| 5 | 4 (l1) | nil | 4 | ... → 3 → 4 |
| 6 | 4 (l2) | nil | nil | ... → 4 → 4 |
最终返回 dummy.next 即 1→1→2→3→4→4 ✅
✅ 答案有效性证明
我们需证明合并后的链表满足:
- 包含所有原节点 :两种方法均遍历了
l1和l2的所有节点,且未跳过任何节点; - 保持升序:每一步都选择当前最小节点,由数学归纳法可知整体有序;
- 正确终止:当任一链表为空时,直接拼接另一链表(其本身有序),保证完整性。
因此,算法正确。
📊 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 递归 | O(m + n) | O(m + n) | 递归调用栈深度为 m+n |
| 迭代 | O(m + n) | O(1) | 仅使用常数额外空间 |
其中 m、n 分别为两个链表的长度。
📌 问题总结
- 关键技巧 :
- 递归:将大问题分解为"选头 + 合并剩余";
- 迭代:使用 虚拟头节点(dummy) 避免处理空链表的边界情况。
- 适用场景 :
- 链表合并是归并排序的核心步骤;
- 在多路归并、K个有序链表合并等问题中会复用此思想。
- 延伸思考 :
- 若扩展为合并 K 个有序链表,可使用优先队列(堆)优化。
💡 建议:面试中优先写迭代解法(空间更优),若时间充裕可补充递归思路展示思维广度。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions