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

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


