【力扣100题】20.合并 K 个升序链表

一、题目描述

给定一个链表数组,每个链表都已经按升序排列。请将所有链表合并到一个升序链表中,返回合并后的链表。

示例 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)
记忆口诀 步长翻倍、两两合并、收敛到一

相关推荐
闻缺陷则喜何志丹1 小时前
【动态规划 前缀和】P7074 [CSP-J2020] 方格取数|普及+
c++·算法·前缀和·动态规划·洛谷
Liangwei Lin2 小时前
LeetCode 74. 搜索二维矩阵
算法·leetcode·矩阵
phltxy2 小时前
Redis Hash 数据类型:详解命令与实战场景
redis·算法·哈希算法
放羊郎9 小时前
基于ORB-SLAM2算法的优化工作
人工智能·算法·计算机视觉
mask哥9 小时前
力扣算法java实现汇总整理(上)
java·算法·leetcode
如果'\'真能转义说10 小时前
OOXML 文档格式剖析:哈希、ZIP结构与识别
xml·算法·c#·哈希算法
梦梦代码精12 小时前
BuildingAI 上部署自定义工作流智能体:5 个实用技巧
大数据·人工智能·算法·开源软件
Zephyr_013 小时前
Leedcode算法题
java·算法