BM5 合并k个已排序的链表¶
讲解章节:分治的三步框架
一句话
小根堆里始终只放 k 个「当前头」,每次弹最小的接走。
解题思路¶
这题考什么¶
把 BM4 的两路归并推广到 k 路。逐条往结果里并要 O(nk):第 i 次合并时 结果链已有的节点会被重新扫一遍。观察 BM4 的循环,每一步其实只做一件事—— 「在所有链的当前头里挑最小」——k 路就用小根堆(每次能 O(log k) 取出最小元素 的完全二叉树结构,Python 标准库 heapq,见 优先队列与堆) 来做这个挑选。堆里永远只有 k 个元素,每条链贡献一个头,弹出一个就补上它的后继。
入堆元素做成三元组 (val, 序号, 节点):heapq 比较元组时前一项相等就接着比后一项,
而 ListNode 没有实现 lt,val 撞车就会直接 TypeError。中间这个全局递增的
序号是破平键,保证比较最迟在第二项分出胜负,永远轮不到节点参与比较。
另一条路是分治两两合并,复杂度同为 O(n log k),但要多写一层递归/分层循环, 而堆的写法一趟到底、常数更小。
数据规模与复杂度¶
节点总数 n <= 5000,|val| <= 1000,k 由输入的链表条数决定,其他语言时限 2 秒。 每个节点入堆一次、出堆一次,各 O(log k),总计 O(n log k); 堆里最多 k 个三元组,额外空间 O(k),结果链复用原节点不新建。 朴素的逐条归并在 k 条等长链时是 5000 * k 级别的重复扫描, 本题 n 只有 5000 还能过,但堆的写法不受 k 增长影响,是可以带走的通法。
坑在哪¶
- 三元组里的破平键不能省。少了它,两条链的当前头 val 相同时 heapq 会去比较 两个 ListNode,抛 TypeError: '<' not supported between instances of 'ListNode';
- 破平键的起始值取 len(lists) 而不是 len(heap)。入参里夹着的空链表在建堆时 被过滤掉了,heap 比 lists 短,用 len(heap) 当起点会和已经在堆里的下标撞车, 撞车的那一刻破平键失效,又退回去比较 ListNode;
- 入参可能是空列表,也可能整条是 None(题面允许链表为空)。建堆时统一用
if node过滤,堆为空则 while 一次不进,返回 dummy.next = None,就是空表; - 结尾的
tail.next = None在正常输入下是空操作——循环退出说明最后弹出的节点 没有后继——但它把「结果链尾一定终止」这件事写死在代码里, 是零成本的防御,读者不必再去反推最后一个节点的 next 是什么。
样例复核¶
[{1,2},{1,4,5},{6}]。堆初始为 (1,0,·)、(1,1,·)、(6,2,·),两个 1 靠破平键分先后, 先弹链 0 的 1,补进 (2,3,·);再弹链 1 的 1,补进 (4,4,·); 之后依次弹 2、4、5、6,得到 {1,1,2,4,5,6},与样例一致。