一、题目描述
给定一个链表数组,每个链表都已经按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:
链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到:
1->1->2->3->4->4->5->6
示例 2:
输入:lists = []
输出:[]
示例 3:
输入:lists = [[]]
输出:[]
提示:
- k == lists.length
- 0 <= k <= 10^4
- 0 <= lists[i].length <= 500
- -10^4 <= lists[i][j] <= 10^4
- lists[i] 按升序排列
- lists[i].length 的总和不超过 10^4
二、解题思路总览
核心思想:分治归并(自底向上)
本题有多种解法:
- 优先级队列(堆)
- 分治归并
本题代码采用的是分治归并思路,和第 148 题(排序链表)是同一套思想,只是扩展到了 K 个链表。
| 方法 | 核心思路 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 堆(优先级队列) | 每次从 K 个链表头取最小 | O(n log k) | O(k) |
| 分治归并 | 两两合并,逐层翻倍 | O(n log k) | O(log k) |
时间复杂度两者相同,分治归并的优势是不需要额外的数据结构。
三、完整代码
cpp
class Solution {
// 合并两个有序链表(与第 21 题完全相同)
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* dummy = new ListNode(0);
ListNode* cur = dummy;
while (list1 && list2) {
if (list1->val > list2->val) {
cur->next = list2;
list2 = list2->next;
} else {
cur->next = list1;
list1 = list1->next;
}
cur = cur->next;
}
cur->next = list1 ? list1 : list2;
return dummy->next;
}
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
int m = lists.size();
if (m == 0) return NULL;
// 按步长翻倍进行两两合并
for (int step = 1; step < m; step *= 2) {
for (int i = 0; i + step < m; i += step * 2) {
lists[i] = mergeTwoLists(lists[i], lists[i + step]);
}
}
return lists[0];
}
};
四、算法流程图
4.1 mergeKLists 主函数流程
输入:链表数组 lists,长度为 m
[Step 1] 判断边界
|
v
m == 0 ?
|是 |否
v v
返回 NULL [Step 2] 继续
|
v
【按步长翻倍进行两两合并】
|
v
step = 1
|
v
step < m 成立?
|否
v
【返回 lists[0]】
|
v
【外层循环:step *= 2】
|
v
step < m 成立?
|否 |是
v v
【返回】 [Step 3] 内层循环
|
v
i = 0
|
v
i + step < m ?
|否
v
【内层循环结束】
|
v
i += step * 2
|
v
回到 i + step < m 判断
4.2 两两合并流程(step = 1 的第一轮)
初始状态:lists = [L0, L1, L2, L3, L4, L5, L6, L7]
step = 1 时:
i = 0: lists[0] = merge(L0, L1)
i = 2: lists[2] = merge(L2, L3)
i = 4: lists[4] = merge(L4, L5)
i = 6: lists[6] = merge(L6, L7)
合并过程:
L0 -> 1 -> 4 -> 5
L1 -> 1 -> 3 -> 4
merge 得到:1 -> 1 -> 3 -> 4 -> 4 -> 5
结果:lists = [合并后L01, 合并后L23, 合并后L45, 合并后L67, L4, L5, L6, L7]
4.3 完整合并树状流程(8 个链表的例子)
初始状态:
lists[0] = 1->4->5
lists[1] = 1->3->4
lists[2] = 2->6
lists[3] = 7->8->9
lists[4] = 10->11
lists[5] = 12->13
lists[6] = 14->15
lists[7] = 16->17
step = 1(第一轮两两合并):
merge(lists[0], lists[1]) --> 1->1->3->4->4->5
merge(lists[2], lists[3]) --> 2->6->7->8->9
merge(lists[4], lists[5]) --> 10->11->12->13
merge(lists[6], lists[7]) --> 14->15->16->17
step = 2(第二轮两两合并):
merge(lists[0], lists[2]) --> 1->1->2->3->4->4->5->6->7->8->9
merge(lists[4], lists[6]) --> 10->11->12->13->14->15->16->17
step = 4(第三轮两两合并):
merge(lists[0], lists[4]) --> 最终结果
4.4 合并两个有序链表示意图
输入:list1 = 1->3->5->7
list2 = 2->4->6->8
初始化:
dummy -> NULL
cur -> dummy
list1 -> 1->3->5->7
list2 -> 2->4->6->8
第一轮比较:1 < 2
cur->next = list1 (1)
cur = cur->next
list1 = list1->next (指向3)
第二轮比较:3 > 2
cur->next = list2 (2)
cur = cur->next
list2 = list2->next (指向4)
第三轮比较:3 < 4
cur->next = list1 (3)
...
重复直到 list1 和 list2 全部处理完
结果:1->2->3->4->5->6->7->8
五、逐行解析
5.1 mergeTwoLists:合并两个有序链表
原理:
- 创建 dummy 哑节点,简化边界处理
- 用 cur 遍历,比较两个链表的当前节点
- 小的节点接入新链表
- 最后把剩余的部分直接接上去
循环条件: while (list1 && list2)
选择逻辑:
if list1->val > list2->val:
选 list2
else:
选 list1
收尾: cur->next = list1 ? list1 : list2
将剩余非空链表直接接入
5.2 mergeKLists 主函数核心逻辑
外层循环控制合并间隔:
for (int step = 1; step < m; step *= 2)
每轮 step 翻倍:1, 2, 4, 8, ...
每轮内部:
for (int i = 0; i + step < m; i += step * 2)
lists[i] = merge(lists[i], lists[i + step])
为什么 step < m?
当 step >= m 时,说明所有链表已经合并完毕,不需要继续。
为什么 i + step < m?
第 i 个链表要和第 i + step 个链表合并,如果 i + step >= m,说明第 i + step 个链表不存在,不需要合并。
六、复杂度分析
6.1 时间复杂度推导
假设有 K 个链表,总节点数为 N。
第一轮(step = 1):合并 K/2 组,每组合并约 2 个节点
第二轮(step = 2):合并 K/4 组,每组合并约 4 个节点
第三轮(step = 4):合并 K/8 组,每组合并约 8 个节点
...
总合并次数 = K/2 + K/4 + K/8 + ... = K
每轮总工作量 = O(N)
总轮数 = log K
总时间 = O(N log K)
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(N log K) | N 为总节点数,K 为链表个数 |
| 空间复杂度 | O(log K) | 递归栈深度(本题用迭代,无递归栈) |
6.2 对比三种解法
| 解法 | 时间 | 空间 | 特点 |
|---|---|---|---|
| 逐个合并 | O(KN) | O(1) | 每次合并后还要和下一个合并 |
| 堆(优先级队列) | O(N log K) | O(K) | 需要维护大小为 K 的堆 |
| 分治归并(本题) | O(N log K) | O(log K) | 无需额外数据结构 |
七、面试追问
| 问题 | 回答要点 |
|---|---|
| 分治归并的时间复杂度是多少? | O(N log K),N 是总节点数,K 是链表个数 |
| 为什么不用逐个合并? | 逐个合并是 O(KN),K 个链表需要合并 K-1 次,每次 O(N) |
| 堆解法和分治解法哪个更好? | 时间复杂度相同,分治解法空间更优(不需要堆) |
| 为什么 step 要翻倍? | 保证每轮合并后,链表数量减半,最终收敛到 1 |
| i += step * 2 是什么意思? | 每次跳过一个完整的合并区间,进入下一个区间 |
| 如果 K 不是 2 的幂次怎么办? | i + step < m 会自动处理边界,只合并存在的配对 |
| i + step < m 而不是 i < m - step? | 两者等价,前者更简洁直观 |
| 能否用递归实现分治? | 可以,但迭代版本更省空间(避免递归栈) |
八、相关题目
| 题号 | 题目 | 关键点 |
|---|---|---|
| 21 | 合并两个有序链表 | mergeTwoLists 基础版 |
| 23 | 合并 K 个升序链表 | 本题 |
| 148 | 排序链表 | 归并排序在链表上的应用 |
| 143 | 重排链表 | 先找中点再合并 |
| 234 | 回文链表 | 快慢指针找中点 |
| 876 | 链表的中间节点 | 快慢指针 |
九、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 分治归并,两两合并,逐层翻倍 |
| 关键循环 | 外层 step *= 2,内层 i += step * 2 |
| 合并函数 | 与第 21 题完全相同的 mergeTwoLists |
| 复杂度 | 时间 O(N log K),空间 O(log K) |
| 记忆口诀 | 步长翻倍、两两合并、收敛到一 |