LeetCode 23. 合并 K 个升序链表
本节目标
用小根堆维护每条链表当前的头部候选,将合并两个有序链表推广到多路归并。
这道题是数据结构综合应用框架中的拓展题。它把链表操作与小根堆结合起来:链表提供有序的后继关系,堆负责在多条链表的当前候选中选出最小值。
题意与约束
给定一个链表数组,每条链表都按升序排列。请把所有节点合并为一条升序链表并返回头节点。数组中可能包含空链表,不同链表中也可能出现相同数值。
如果只有两条链表,可以用两个指针比较当前节点;当链表数量扩展到 k 条时,核心问题变成:如何高效找出 k 个当前头节点中的最小者。
从两路归并到多路归并
任意时刻,每条尚未耗尽的链表只有头部节点可能成为全局下一个节点。因为链表内部有序,头部之后的节点不可能越过自己的头部提前被选择。
因此,小根堆中不需要放入全部节点,只需要放入每条非空链表当前尚未处理的第一个节点:
- 初始化时,把每条非空链表的头节点加入堆;
- 弹出值最小的节点,把它接到结果链表末尾;
- 若该节点还有后继,就把后继加入堆;
- 重复直到堆为空。
核心不变量
每轮开始时,堆中恰好保存每条未耗尽链表的最小未处理节点。因此:
- 堆顶一定是所有未处理节点中的全局最小值;
- 弹出堆顶不会破坏结果链表的升序;
- 只需补入同一条链表的后继,就能恢复不变量。
堆大小最多为 k,而不是所有节点总数。这正是利用各链表内部有序性减少候选数量的关键。
相同节点值如何比较
不同链表的当前节点可能具有相同的 val。C++ 的比较器只按节点值确定优先级,相等时任意一个先出堆都不影响结果。
Python 的堆会在元组第一项相等时继续比较下一项,而两个 ListNode 对象本身不可比较。因此实现中加入递增序号,保存 (节点值, 序号, 节点)。序号只负责打破平局,不改变算法含义。
代码实现
两份代码都复用原链表节点,不新建每个结果节点;哑节点只用于统一处理结果链表头部。堆里始终只有各链表的当前头部候选。
- C++
- Python
C++17
#include <queue>
#include <vector>
using namespace std;
class Solution {
private:
struct CompareNode {
bool operator()(ListNode* left, ListNode* right) const {
return left->val > right->val;
}
};
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*, vector<ListNode*>, CompareNode> candidates;
for (ListNode* head : lists) {
if (head != nullptr) {
candidates.push(head);
}
}
ListNode dummy(0);
ListNode* tail = &dummy;
while (!candidates.empty()) {
ListNode* node = candidates.top();
candidates.pop();
tail->next = node;
tail = node;
if (node->next != nullptr) {
candidates.push(node->next);
}
}
return dummy.next;
}
};
Python 3
import heapq
class Solution:
def mergeKLists(self, lists):
candidates = []
sequence = 0
for head in lists:
if head is not None:
heapq.heappush(candidates, (head.val, sequence, head))
sequence += 1
dummy = ListNode()
tail = dummy
while candidates:
_, _, node = heapq.heappop(candidates)
tail.next = node
tail = node
if node.next is not None:
heapq.heappush(
candidates,
(node.next.val, sequence, node.next),
)
sequence += 1
return dummy.next
复杂度分析
设所有链表共有 n 个节点,链表数量为 k。
- 时间复杂度:
O(n log k)。每个节点恰好入堆、出堆各一次,堆大小不超过k。 - 空间复杂度:
O(k)。不计返回链表本身,额外空间主要来自堆。
易错点
- 把所有节点一次性放入堆,虽然可行,但空间增至
O(n),没有利用链表有序性。 - 弹出节点后忘记把它的后继加入堆,导致该链表剩余部分丢失。
- 把空链表头节点加入堆,造成空指针访问。
- Python 直接保存
(node.val, node),遇到相同值时会尝试比较节点对象并报错。 - 接入结果链表时额外复制大量节点,增加不必要的实现复杂度。
模式迁移
“每一路只暴露一个当前候选”的多路归并思想同样适用于:
- 合并多个有序数组或文件;
- 从多个有序序列中寻找第
k小元素; - 外部排序中合并多个已排序数据块。
判断能否使用该模式的关键是:每一路内部是否有序,以及选走当前元素后,能否立刻得到这一路唯一的新候选。