本文概览:本文以LeetCode经典题目"合并 K 个升序链表"为例,从顺序合并的暴力思路入手,分析为什么越早参与合并的节点会被重复遍历,再引出分治合并,将时间复杂度从 O(Nk) 优化到 O(N log k)
一、题目

二、题目分析
给定一个链表数组 lists,其中每个链表都已经按照升序排列,要求将这 k 个升序链表合并成一个新的升序链表
目标:合并多个有序链表,并返回合并后的链表头节点
核心特点:每个链表本身已经有序,所以问题不是排序每个链表,而是如何高效地把多个有序链表合并起来
这道题和上一题"排序链表"很像:
- 排序链表:原链表无序,需要先拆分,再排序合并
- 合并 K 个升序链表:每条链表已经有序,只需要考虑怎么高效合并
最终都离不开一个基础操作:合并两个有序链表
思路概览
Java实现代码如下
Java
public ListNode mergeKLists(ListNode[] lists) {
// 递归找到数组的中间节点
if (lists == null || lists.length == 0)
return null;
return mergeArray(lists, 0, lists.length - 1);
}
private ListNode mergeArray(ListNode[] lists, int left, int right) {
if (left == right)
return lists[left];
// 找到中间节点
int mid = left + (right - left) / 2;
// 递归合并左半部分的链表
ListNode leftNode = mergeArray(lists, left, mid);
// 递归合并右半部分的链表
ListNode rightNode = mergeArray(lists, mid + 1, right);
// 合并左半部分和右半部分的链表
return mergeTwoLists(leftNode, rightNode);
}
private ListNode mergeTwoLists(ListNode left, ListNode right) {
ListNode dummy = new ListNode(0);
dummy.next = left;
// 记录当前节点的位置
ListNode cur = dummy;
while (left != null && right != null) {
// 比较两个节点的值
if (left.val <= right.val) {
// 将当前节点指向左节点
cur.next = left;
// 左节点指针向右移动
left = left.next;
} else {
// 将当前节点指向右节点
cur.next = right;
// 右节点指针向右移动
right = right.next;
}
cur = cur.next;
}
cur.next = left != null ? left : right;
return dummy.next;
}
思路简要说明
-
分治拆分链表数组 :将
lists按中点分成左右两半,递归处理 -
递归合并左右结果:左半部分合并成一个有序链表,右半部分合并成一个有序链表
-
合并两个有序链表 :用和"排序链表"中一样的
mergeTwoLists方法,将左右两个有序链表合并成最终链表
三、思路详解
暴力解法入手:顺序合并
最直观的做法是:遍历链表数组,每次拿当前结果链表和下一个链表进行合并
比如有 4 个链表:
L1, L2, L3, L4
顺序合并就是:
先合并:merge(L1, L2) → M12
再合并:merge(M12, L3) → M123
再合并:merge(M123, L4) → M1234
这个思路很简单,因为我们已经会合并两个有序链表了。问题在于:越早参与合并的节点,会被反复遍历很多次
假设每条链表长度都为 n,一共有 k 条链表:
- 第一次合并:
L1 + L2,需要遍历2n个节点 - 第二次合并:
M12 + L3,需要遍历3n个节点 - 第三次合并:
M123 + L4,需要遍历4n个节点 - ...
- 最后一次合并:需要遍历
kn个节点
总操作次数大约是:
2n + 3n + 4n + ... + kn
= n × (2 + 3 + 4 + ... + k)
≈ O(n × k²)
如果总节点数记为 N = n × k,那么复杂度就是 O(Nk)
核心瓶颈 :越早加入结果链表的节点,被重复遍历的次数越多。比如 L1 中的节点,从第一次合并开始,每一轮都会参与后续合并,重复参与了很多次
关键思考:能不能让每个链表参与合并的次数更平均,而不是让前面的链表被反复合并?
分治优化思路
顺序合并的问题在于合并过程太"偏":一直拿已经合并好的大链表去合并新的小链表,导致大链表越来越大,重复遍历越来越多
更合理的方式是:像归并排序一样,采用分治
分治的思路是:
- 把
k个链表从中间分成左右两半 - 左半部分合并成一个有序链表
- 右半部分合并成一个有序链表
- 最后再把左右两个有序链表合并
也就是:
L1, L2, L3, L4, L5, L6, L7, L8
先分成两半:
[L1, L2, L3, L4] [L5, L6, L7, L8]
继续分:
[L1, L2] [L3, L4] [L5, L6] [L7, L8]
继续分到单个链表:
L1 L2 L3 L4 L5 L6 L7 L8
开始合并:
merge(L1,L2), merge(L3,L4), merge(L5,L6), merge(L7,L8)
再合并:
merge(M12,M34), merge(M56,M78)
最后合并:
merge(M1234,M5678)
这个过程可以看成一棵二叉树:
merge全部
/ \
merge左半 merge右半
/ \ / \
merge12 merge34 merge56 merge78
/ \ / \ / \ / \
L1 L2 L3 L4 L5 L6 L7 L8
二叉树高度是 log₂k,也就是说每个节点最多只会参与 log₂k 层合并,而不是像顺序合并那样,前面的节点可能参与接近 k 次合并
为什么分治后复杂度是 O(N log k)?
设所有链表的节点总数为 N,链表数量为 k
在分治合并中:
- 每一层合并,所有节点总共只会被遍历一次,所以每层代价是
O(N) - 一共有
log₂k层合并
所以总时间复杂度是:
O(N) × O(log k) = O(N log k)
这比顺序合并的 O(Nk) 好很多,尤其当链表数量 k 很大时,差距非常明显
举个简单对比:
- 顺序合并:节点可能被重复遍历接近 k 次
- 分治合并:节点只会参与 log₂k 层合并
当 k = 1024 时:
- 顺序合并最多接近 1024 层重复合并
- 分治合并只有 log₂1024 = 10 层
这就是分治优化的核心收益
代码一:递归拆分链表数组
主函数只做两件事:
- 处理空数组
- 调用递归函数合并整个数组范围
Java
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0)
return null;
return mergeArray(lists, 0, lists.length - 1);
}
mergeArray(lists, left, right) 表示:把 lists[left..right] 范围内的所有链表合并成一个有序链表
Java
private ListNode mergeArray(ListNode[] lists, int left, int right) {
if (left == right)
return lists[left];
int mid = left + (right - left) / 2;
ListNode leftNode = mergeArray(lists, left, mid);
ListNode rightNode = mergeArray(lists, mid + 1, right);
return mergeTwoLists(leftNode, rightNode);
}
这里的逻辑非常像"排序链表":
left == right:说明当前范围只有一个链表,它本身已经有序,直接返回- 找中点
mid - 递归合并左半部分
- 递归合并右半部分
- 最后合并左右两个有序链表
代码二:合并两个有序链表
合并两个有序链表的代码和上一题排序链表中的合并逻辑是一样的
Java
private ListNode mergeTwoLists(ListNode left, ListNode right) {
ListNode dummy = new ListNode(0);
dummy.next = left;
ListNode cur = dummy;
while (left != null && right != null) {
if (left.val <= right.val) {
cur.next = left;
left = left.next;
} else {
cur.next = right;
right = right.next;
}
cur = cur.next;
}
cur.next = left != null ? left : right;
return dummy.next;
}
核心思路是:
left和right分别指向两个有序链表当前节点- 每次比较
left.val和right.val - 谁更小,就把谁接到结果链表后面
- 被接上的链表指针向后移动
- 最后把剩余链表直接接到结果后面
这里用了 dummy 哑节点,是为了简化头节点处理。cur 始终指向结果链表的尾节点
举例:
left: 1 → 4 → 5
right: 1 → 3 → 4
合并过程:
| 步骤 | left | right | 选择 | 结果链表 |
|---|---|---|---|---|
| 1 | 1 | 1 | left的1 | 1 |
| 2 | 4 | 1 | right的1 | 1 → 1 |
| 3 | 4 | 3 | right的3 | 1 → 1 → 3 |
| 4 | 4 | 4 | left的4 | 1 → 1 → 3 → 4 |
| 5 | 5 | 4 | right的4 | 1 → 1 → 3 → 4 → 4 |
| 6 | 5 | null | 接上left剩余 | 1 → 1 → 3 → 4 → 4 → 5 |
完整分治流程示例
假设有 4 个链表:
L1: 1 → 4 → 5
L2: 1 → 3 → 4
L3: 2 → 6
L4: 0 → 7
分治过程如下:
mergeArray(0,3)
├── mergeArray(0,1)
│ ├── L1
│ ├── L2
│ └── merge(L1,L2) → 1→1→3→4→4→5
├── mergeArray(2,3)
│ ├── L3
│ ├── L4
│ └── merge(L3,L4) → 0→2→6→7
└── merge(左半结果, 右半结果)
→ 0→1→1→2→3→4→4→5→6→7
可以看到,分治不是从左到右一个一个合并,而是先让相邻链表两两合并,再逐层往上合并。这样每一层处理的总节点数都是 N,层数是 log k
四、复杂度分析
设:
k为链表数量N为所有链表节点总数
时间复杂度:O(N log k)
每一层合并会遍历所有节点一次,总代价 O(N);分治树高度为 O(log k),所以总时间复杂度为 O(N log k)
空间复杂度:O(log k)
递归调用栈深度是 log k。如果不考虑递归栈,合并链表本身只使用常数指针变量
五、总结
这道题的关键是:不要顺序合并,否则越早加入的链表会被反复遍历,时间复杂度会退化到 O(Nk)
分治优化的核心是:
- 把链表数组不断从中间拆分
- 拆到单个链表时直接返回
- 左右两边分别合并成有序链表
- 最后用合并两个有序链表的方法合并左右结果
本题本质上和"排序链表"一样,都依赖归并思想,只不过:
- 排序链表是先拆一个无序链表,再合并排序
- 合并 K 个升序链表是拆链表数组,再合并多个已经有序的链表