leetcode
leetcode 1 ~ 50
合并 K 个升序链表

合并 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

代码结果

运行时间: 68 ms, 内存: 20.1 MB


// Java Stream solution to merge k sorted linked lists
// The idea is to convert the linked lists into streams, merge them, sort the combined stream, and then convert it back into a linked list
 
import java.util.*;
import java.util.stream.*;
 
class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}
 
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        return Arrays.stream(lists)
                     .filter(Objects::nonNull)
                     .flatMap(this::toStream)
                     .sorted()
                     .map(ListNode::new)
                     .reduce((a, b) -> { a.next = b; return b; })
                     .orElse(null);
    }
 
    // Helper method to convert a linked list to a stream of integers
    private Stream<Integer> toStream(ListNode node) {
        List<Integer> result = new ArrayList<>();
        while (node != null) {
            result.add(node.val);
            node = node.next;
        }
        return result.stream();
    }
}
 

解释

方法:

这个题解使用了优先队列(最小堆)来合并 K 个有序链表。首先,我们创建一个虚拟头节点 dummy,并将指针 p 指向 dummy。然后,我们遍历所有的链表头节点,将非空的头节点的值、链表编号和节点本身作为一个三元组放入最小堆中。接下来,我们不断从最小堆中取出最小值节点,将其添加到合并后的链表中,并将指针 p 向后移动。如果取出的节点还有下一个节点,我们将下一个节点的信息放入最小堆中。重复这个过程,直到最小堆为空。最后,我们返回 dummy.next,即合并后的链表的真正头节点。

时间复杂度:

O((n + k) * log(k))

空间复杂度:

O(k)

代码细节讲解

🦆
在这个解法中,你是如何确保在最小堆中各节点始终保持正确的排序顺序?
最小堆(优先队列)是一种数据结构,它保证每次从中取出的元素都是具有最小值的元素。在本题解中,我使用了三元组来存储每个节点的值、其所在链表的索引和节点对象本身,并将这些三元组插入到最小堆中。最小堆会根据三元组的第一个元素(节点的值)自动维护顺序,确保每次取出的都是当前堆中值最小的节点。这样,每次取出的节点都是当前未被合并链表中值最小的节点,保持了整体的排序正确性。
🦆
为什么选择使用优先队列(最小堆)而不是直接遍历所有链表进行比较合并?
直接遍历所有链表进行比较合并(如依次比较每个链表的头节点,然后选择最小的一个)会产生较高的时间复杂度,尤其是当链表数量很多时。使用最小堆可以更有效地管理和比较头节点,因为最小堆可以在对数时间内插入和删除元素,即 O(log(k)),其中 k 是链表的数量。这使得整个合并过程的时间复杂度降低到 O((n + k) * log(k)),其中 n 是所有链表中节点的总数。这种方法比直接遍历所有链表更高效,尤其是在链表数量较多或链表长度不均时。
🦆
你提到使用了虚拟头节点,这样做的主要目的是什么?
使用虚拟头节点的主要目的是为了简化链表操作。虚拟头节点作为合并后链表的起始点,可以避免处理空链表的边界情况,并且使得链表节点的添加操作统一化,无需检查当前合并链表是否为空。这样,可以直接在虚拟头节点后开始插入节点,最后返回虚拟头节点的下一个节点作为合并后的链表的头节点。
🦆
在取出最小堆中的节点时,你提到了'取出节点的下一个节点',请问这个步骤如何确保不会引入已排序部分的破坏?
当从最小堆中取出一个节点后,此节点已经是当前最小的节点,我将其添加到合并后的链表中。随后,如果这个节点还有下一个节点,我将这个下一个节点放入最小堆中以供后续操作。由于最小堆保证每次都会取出最小元素,且每个链表本身是有序的,这个操作不会破坏已排序的部分。这样确保了整个链表在合并时仍然保持有序。
🦆
整个合并过程中,对于链表头节点为空的情况,你是如何处理的?
在初始化最小堆的时候,我会检查每个链表的头节点。如果一个链表的头节点为空(即该链表为空),则我不会将其加入到最小堆中。这样,只有非空链表的头节点才会被加入到堆中,从而确保在后续的处理中不会遇到空链表带来的问题。这个策略简化了代码的复杂性,并防止了在处理过程中遇到空指针的错误。
🦆
题解中提到了时间复杂度和空间复杂度,请问这种算法在最坏情况下的表现如何?
在最坏情况下,即所有链表的节点数之和为 n,链表的个数为 k 时,时间复杂度为 O((n + k) * log(k))。这是因为每个节点和每个链表的头节点都需要经过最小堆的操作。空间复杂度为 O(k),因为最小堆中最多同时存储 k 个节点的信息。这种算法对于节点数量和链表数量较大的情况是效率较高的,尤其是在节点分布较均匀时。

相关问题

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 

 

示例 1:

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

示例 2:

输入:l1 = [], l2 = []
输出:[]

示例 3:

输入:l1 = [], l2 = [0]
输出:[0]

 

提示:

  • 两个链表的节点数目范围是 [0, 50]
  • -100 <= Node.val <= 100
  • l1l2 均按 非递减顺序 排列

丑数 II

给你一个整数 n ,请你找出并返回第 n丑数

丑数 就是质因子只包含 23 和 5 的正整数。

 

示例 1:

输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。

示例 2:

输入:n = 1
输出:1
解释:1 通常被视为丑数。

 

提示:

  • 1 <= n <= 1690