优先队列题目:合并K个升序链表

文章目录

  • 题目
    • 标题和出处
    • 难度
    • 题目描述
      • 要求
      • 示例
      • 数据范围
  • 解法
    • 思路和算法
    • 代码
    • 复杂度分析

题目

标题和出处

标题:合并K个升序链表

出处:23. 合并K个升序链表

难度

6 级

题目描述

要求

给你一个包含 k \texttt{k} k 个链表的数组 lists \texttt{lists} lists,每个链表都已经按升序排列。

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

示例

示例 1:

输入: lists   =   [[1,4,5],[1,3,4],[2,6]] \texttt{lists = [[1,4,5],[1,3,4],[2,6]]} lists = [[1,4,5],[1,3,4],[2,6]]
输出: [1,1,2,3,4,4,5,6] \texttt{[1,1,2,3,4,4,5,6]} [1,1,2,3,4,4,5,6]
解释:链表数组如下:
[ \texttt{[} [
   1 → 4 → 5, \texttt{~~1} \rightarrow \texttt{4} \rightarrow \texttt{5,}   145,
   1 → 3 → 4, \texttt{~~1} \rightarrow \texttt{3} \rightarrow \texttt{4,}   134,
   2 → 6 \texttt{~~2} \rightarrow \texttt{6}   26
] \texttt{]} ]
将它们合并到一个有序链表中得到:
1 → 1 → 2 → 3 → 4 → 4 → 5 → 6 \texttt{1} \rightarrow \texttt{1} \rightarrow \texttt{2} \rightarrow \texttt{3} \rightarrow \texttt{4} \rightarrow \texttt{4} \rightarrow \texttt{5} \rightarrow \texttt{6} 11234456

示例 2:

输入: lists   =   [] \texttt{lists = []} lists = []
输出: [] \texttt{[]} []

示例 3:

输入: lists   =   [[]] \texttt{lists = [[]]} lists = [[]]
输出: [] \texttt{[]} []

数据范围

  • k = lists.length \texttt{k} = \texttt{lists.length} k=lists.length
  • 0 ≤ k ≤ 10 4 \texttt{0} \le \texttt{k} \le \texttt{10}^\texttt{4} 0k104
  • 0 ≤ lists[i].length ≤ 500 \texttt{0} \le \texttt{lists[i].length} \le \texttt{500} 0lists[i].length500
  • -10 4 ≤ lists[i][j] ≤ 10 4 \texttt{-10}^\texttt{4} \le \texttt{lists[i][j]} \le \texttt{10}^\texttt{4} -104lists[i][j]104
  • lists[i] \texttt{lists[i]} lists[i]升序排列
  • lists[i].length \texttt{lists[i].length} lists[i].length 的总和不超过 10 4 \texttt{10}^\texttt{4} 104

解法

思路和算法

这道题是「合并两个有序链表」的进阶,要求将 k k k 个升序链表合并成一个升序链表。合并链表的方法是,每次从所有剩余的非空链表中找到值最小的结点,将该结点合并到结果链表中,然后将对应链表的结点向后移动一步,重复该过程直到所有的链表都为空或者只有一个链表不为空。如果所有的链表都为空,则合并结束;如果只有一个链表不为空,由于该链表一定是升序的,因此直接将该链表合并到结果链表的末尾即可。

由于合并后的链表的头结点为 k k k 个链表的头结点中的值最小的结点,因此需要创建哑结点 dummyHead \textit{dummyHead} dummyHead,将合并的结点依次添加到哑结点的后面。具体做法是,创建指针 temp \textit{temp} temp 表示上一个合并的结点,初始时 temp \textit{temp} temp 指向 dummyHead \textit{dummyHead} dummyHead,每次合并结点时,将合并的结点添加到 temp \textit{temp} temp 的后面,然后将 temp \textit{temp} temp 向后移动一步,直到合并完毕。合并结束之后, dummyHead . next \textit{dummyHead}.\textit{next} dummyHead.next 即为合并后的新链表的头结点。

为了得到所有剩余的非空链表中的值最小的结点,可以遍历全部非空链表找到值最小的结点,但是该做法每次合并一个结点都需要遍历全部非空链表,因此单次合并操作的时间复杂度为 O ( k ) O(k) O(k)。可以使用优先队列将单次合并操作的时间复杂度降到 O ( log ⁡ k ) O(\log k) O(logk),优先队列存储结点,队首结点为值最小的结点。

首先遍历 k k k 个链表,将每个非空链表的头结点加入优先队列,然后进行合并操作。每次合并操作时,首先从优先队列中取出剩余结点中的值最小的结点,将该结点合并到结果链表中,然后判断该结点的后一个结点是否为空,如果不为空则加入优先队列。合并操作可以保证每个链表最多有一个结点在优先队列中,且优先队列中的结点为每个链表的第一个尚未合并的结点。如果一个链表的全部结点都已经合并,则优先队列中不会有该链表的任何结点。

代码

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists.length == 0) {
            return null;
        }
        ListNode dummyHead = new ListNode(0);
        PriorityQueue<ListNode> pq = new PriorityQueue<ListNode>(new Comparator<ListNode>() {
            public int compare(ListNode node1, ListNode node2) {
                return node1.val - node2.val;
            }
        });
        for (ListNode head : lists) {
            if (head != null) {
                pq.offer(head);
            }
        }
        ListNode temp = dummyHead;
        while (pq.size() > 1) {
            ListNode node = pq.poll();
            temp.next = node;
            temp = temp.next;
            node = node.next;
            if (node != null) {
                pq.offer(node);
            }
        }
        if (!pq.isEmpty()) {
            temp.next = pq.poll();
        }
        return dummyHead.next;
    }
}

复杂度分析

  • 时间复杂度: O ( n log ⁡ k ) O(n \log k) O(nlogk),其中 n n n 是全部链表的结点数, k k k 是原始链表的个数。由于优先队列中最多有 k k k 个结点,每次将结点加入优先队列和从优先队列中取出的时间复杂度是 O ( log ⁡ k ) O(\log k) O(logk),每个结点都会被加入优先队列和从优先队列中取出各一次,因此总时间复杂度是 O ( n log ⁡ k ) O(n \log k) O(nlogk)

  • 空间复杂度: O ( 1 ) O(1) O(1)。除了返回值以外,使用的空间复杂度是常数。

你可能感兴趣的:(数据结构和算法,#,栈和队列,链表,队列,优先队列)