#23 堆(优先队列) 困难

合并 K 个升序链表

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

示例 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 = [[]]
输出:[]

💭 模拟答题者思考

1. 我先想暴力:把所有节点值收集到数组里排序,再逐个新建节点串起来——能过,但完全没用「每条链表已有序」这一条件,时间 O(N log N)、额外空间 O(N)。

2. 重复在哪里?每次只需要在 k 条链表的「当前头节点」里找全局最小者接到结果尾部,然后该链表指针前移——这和合并两个有序链表一样,只是候选从 2 个变成 k 个。

3. 朴素做法:依次把 lists[0] 与 lists[1] 合并、再与 lists[2] 合并……复用 #21 的双指针合并,最坏时间 O(kN)(N 为总节点数),k 很大时偏慢。

4. 优化成最小堆:把 k 个头节点放入堆,每次弹出堆顶(全局最小)接到 curr.next,若该节点有后继则把后继入堆——每个节点恰好入堆、出堆一次,时间 O(N log k)。

5. 另一种 O(N log k) 是分治:两两归并像归并排序,但堆解法更直观,且 k 条链表头随时变化时堆天然适配。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
dummyListNode*定义:哨兵头节点,不存放有效值
维护:始终位于合并结果链表的最前端,统一处理「结果为空」等边界
更新:创建后不再移动,最终返回 dummy.next
currListNode*定义:合并结果链表的尾指针
维护:指向已拼接部分的最后一个节点,新节点总是接在 curr.next
更新:每从堆中取出节点并接入后 curr = curr.next
heapmin-heap定义:存放各链表当前头节点的最小堆,堆顶始终是全局最小值
维护:初始化时把每条非空链表的头节点入堆;每弹出一个节点后,若该节点还有后继则把后继入堆
更新heappop 取出最小节点;heappush(node.next) 补充下一位候选
(val, i, node)tuple定义:堆元素的排序键——节点值、链表编号、节点指针
维护:值相等时用编号 i 打破平局,避免 Python 比较两个 ListNode 对象
更新:每次入堆时按当前头节点的三元组构造

⌨️ 落码步骤

1. 创建哨兵 dummycurr = dummy;初始化空堆 heap

2. 遍历 lists:对每条非空链表,将 (node.val, i, node) 入堆

3. 当堆非空:弹出最小元 (val, i, node),挂到 curr.nextcurr = curr.next

4. 若 node.next 非空,将 (node.next.val, i, node.next) 入堆

5. 返回 dummy.next

💻 代码实现

import heapq
from typing import List, Optional

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next

class Solution:
    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        dummy = ListNode(0)   # 哨兵,简化头节点处理
        curr = dummy          # 结果链表的尾指针
        heap: list[tuple[int, int, ListNode]] = []

        for i, node in enumerate(lists):
            if node:
                heapq.heappush(heap, (node.val, i, node))

        while heap:
            _, i, node = heapq.heappop(heap)
            curr.next = node
            curr = curr.next
            nxt = node.next
            if nxt:
                heapq.heappush(heap, (nxt.val, i, nxt))

        return dummy.next
class Solution {
public:
    ListNode* mergeKLists(vector<ListNode*>& lists) {
        auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; };
        priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp);

        for (ListNode* head : lists) {
            if (head) pq.push(head);
        }

        ListNode dummy(0);
        ListNode* curr = &dummy;

        while (!pq.empty()) {
            ListNode* node = pq.top();
            pq.pop();
            curr->next = node;
            curr = curr->next;
            if (node->next) pq.push(node->next);
        }
        return dummy.next;
    }
};
// 时间 O(N log k),空间 O(k)(堆大小)

📈 复杂度分析

时间复杂度 O(N log k)
空间复杂度 O(k)(堆大小)

⚠️ 常见坑

Python 堆直接存 ListNode:值相等时会比较两个节点对象并报错——必须用 (val, i, node) 元组,用编号 i 打破平局。

忘记移动 curr:只设置 curr.next 却不 curr = curr.next,会导致节点叠在同一位置或成环。

空链表未过滤:初始化时须跳过 null 头节点,否则堆中混入空指针;lists = [][[]] 应直接返回空链表。

🔍 必测边界 Case

Case 1:lists 为空数组
lists = [] → []
Case 2:仅含空链表
lists = [[]] → []
Case 3:多条链表等值头节点
lists = [[1,4,5],[1,3,4],[2,6]] → [1,1,2,3,4,4,5,6](堆须正确处理值相等)