一、题目描述
21. 合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:
list1 = [1,2,4],list2 = [1,3,4]输出:
[1,1,2,3,4,4]
示例 2:
输入:
list1 = [],list2 = []输出:
[]
示例 3:
输入:
list1 = [],list2 = [0]输出:
[0]
提示:
-
两个链表的节点数目范围是
[0, 50] -
-100 <= Node.val <= 100 -
list1和list2均按 非递减顺序 排列
二、解题思路概览
合并两个有序链表,最自然的想法是使用双指针归并。常见解法有三种:
| 解法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 迭代归并 | O(m+n) | O(1) | 最优解,面试首选 |
| 递归归并 | O(m+n) | O(m+n)(栈空间) | 代码简洁,思路清晰 |
| 集合排序法 | O((m+n) log(m+n)) | O(m+n) | 不推荐,仅作对比 |
三、解法一:迭代归并(推荐)⭐
3.1 思路
同时遍历两个链表,比较当前节点值,将较小者接入结果链表,并移动对应链表的指针。最后将未遍历完的链表直接接到结果末尾。
3.2 代码实现
java
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 哑结点,简化边界处理
ListNode dummy = new ListNode(0);
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;
}
}
3.3 图解示例
以 list1 = [1,2,4],list2 = [1,3,4] 为例:
text
初始: list1: 1 → 2 → 4
list2: 1 → 3 → 4
dummy → null
第1步: 比较1和1,取list1的1 → dummy → 1, list1指向2
第2步: 比较2和1,取list2的1 → dummy → 1 → 1, list2指向3
第3步: 比较2和3,取list1的2 → ... → 2, list1指向4
第4步: 比较4和3,取list2的3 → ... → 3, list2指向4
第5步: 比较4和4,取list1的4 → ... → 4, list1指向null
最后: 将list2剩余的4接上 → 得到 1→1→2→3→4→4
3.4 复杂度分析
-
时间复杂度:O(m+n),每个节点恰好被处理一次。
-
空间复杂度:O(1),只使用了常数个指针。
四、解法二:递归归并
4.1 思路
递归定义:合并两个链表的结果 = 较小头节点 + 合并剩余部分。递归终止条件:任一链表为空,返回另一个链表。
4.2 代码实现
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;
}
}
}
4.3 复杂度分析
-
时间复杂度:O(m+n),每个节点被递归调用一次。
-
空间复杂度:O(m+n),递归调用栈的深度等于合并后链表的长度。
4.4 递归与迭代对比
| 维度 | 迭代 | 递归 |
|---|---|---|
| 代码长度 | 稍长 | 极简 |
| 空间效率 | O(1) | O(n) |
| 可读性 | 直观 | 需要理解递归 |
| 风险 | 无 | 链表过长可能栈溢出 |
五、解法三:集合排序法(不推荐)
5.1 思路
将两个链表的所有节点值取出放入 ArrayList,排序后再重新构建链表。这种方法完全没有利用链表已有序的特性,效率低下,仅作为错误代码对照。
5.2 常见错误写法及修正
错误代码(来自用户提问):
java
class Solution {
ListNode mergeTwoLists(ListNode list1, ListNode list2) {
List<Integer> list = new ArrayList<>();
for(ListNode pre = list1; pre != null; pre = pre.next) list.add(pre.val);
for(ListNode pre = list2; pre != null; pre = pre.next) list.add(pre.val);
ArrayList<Integer> listsort = new ArrayList<>(list);
Collections.sort(listsort); // 对 listsort 排序
ListNode head = new ListNode(0);
ListNode cur = head;
for(int i = 0; i < list.size(); i++) { // ❌ 错误:用了未排序的 list
cur.next = new ListNode(list.get(i));
cur = cur.next;
}
return head.next;
}
}
错误原因 :排序的是 listsort,构建链表时却用了原始的 list,导致输出顺序仍为"先list1后list2"。
正确修正(但仍不推荐):
java
class Solution {
ListNode mergeTwoLists(ListNode list1, ListNode list2) {
List<Integer> list = new ArrayList<>();
for (ListNode p = list1; p != null; p = p.next) list.add(p.val);
for (ListNode p = list2; p != null; p = p.next) list.add(p.val);
Collections.sort(list); // 直接排序原列表
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
for (int val : list) {
cur.next = new ListNode(val);
cur = cur.next;
}
return dummy.next;
}
}
复杂度:时间 O((m+n) log(m+n)),空间 O(m+n)。远不如归并法,面试时请勿使用。
六、解法对比与总结
| 方法 | 时间复杂度 | 空间复杂度 | 是否利用有序性 | 面试推荐度 |
|---|---|---|---|---|
| 迭代归并 | O(m+n) | O(1) | ✅ | ⭐⭐⭐⭐⭐ |
| 递归归并 | O(m+n) | O(m+n) | ✅ | ⭐⭐⭐⭐ |
| 集合排序 | O(n log n) | O(n) | ❌ | ⭐ |
6.1 面试建议
-
首选迭代归并:空间 O(1),最符合面试官期待。
-
递归可作为备选:代码简洁,但要说明空间代价。
-
切忌使用排序法:即便能通过,也会被质疑基本功。
6.2 常见扩展问题
-
合并 K 个有序链表(LeetCode 23):可使用分治归并或优先队列。
-
合并后仍然保持有序:本题已要求,注意处理相等值时的稳定性(通常无影响)。
-
不允许创建新节点:必须原地拼接,迭代法天然满足。
七、相关链接
-
官方题解 :合并两个有序链表官方题解