一、题目
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
二、解题思路
- 创建一个最小堆(优先队列)来存储所有链表的头节点。这样我们可以始终取出当前所有链表中值最小的节点。
- 初始化一个虚拟头节点(dummy),它将作为合并后链表的头节点。
- 创建一个指针(current)指向虚拟头节点,用于构建新的链表。
- 循环执行以下步骤直到所有链表都被合并: a. 从最小堆中取出最小节点。 b. 将current指针移动到这个最小节点。 c. 将最小节点的next指针指向最小堆中的下一个最小节点,或者如果当前链表已经没有更多节点,则指向null。 d. 将取出的节点的原链表的下一个节点加入到最小堆中,以便后续处理。
- 当所有链表都合并完毕,current指针将指向合并后链表的尾节点。返回虚拟头节点的下一个节点即为合并后的链表。
三、具体代码
import java.util.PriorityQueue;class Solution {public ListNode mergeKLists(ListNode[] lists) {// 创建一个最小堆来存储链表头节点PriorityQueue<ListNode> minHeap = new PriorityQueue<>((a, b) -> a.val - b.val);// 初始化虚拟头节点ListNode dummy = new ListNode(0);ListNode current = dummy;// 初始化最小堆for (ListNode list : lists) {if (list != null) {minHeap.offer(list);}}// 开始合并链表while (!minHeap.isEmpty()) {// 取出最小节点ListNode minNode = minHeap.poll();// 将current指向这个最小节点current.next = minNode;// 移动current指针current = current.next;// 如果还有下一个节点,加入到最小堆中if (minNode.next != null) {minHeap.offer(minNode.next);}}// 返回合并后的链表头节点return dummy.next;}
}// ListNode的定义已经在问题描述中给出
四、时间复杂度和空间复杂度
1. 时间复杂度
-
时间复杂度是O(NlogK)。
-
初始化最小堆的时间复杂度是O(N),其中N是所有链表中元素的总数。这是因为我们需要遍历每个链表的头节点,并将它们加入到最小堆中。对于每个节点,最小堆的插入操作是O(logK),K是链表的数量。但是,由于每个节点最多被插入一次,所以这部分的总时间复杂度是O(N)。
-
合并链表的循环中,我们从最小堆中取出节点,这个操作是O(logK)。然后,我们将取出的节点的下一个节点(如果有的话)再次加入到最小堆中,这个操作同样是O(logK)。这个循环会执行N次,因为我们需要处理所有N个元素。所以,这部分的总时间复杂度是O(NlogK)。
2. 空间复杂度
-
空间复杂度是O(K)。
-
最小堆存储了所有链表的头节点,最多时有K个节点,所以空间复杂度是O(K)。
-
合并后的链表不需要额外的空间,因为它是直接在原有链表的基础上构建的。
-
虚拟头节点dummy是一个常数级别的空间开销。
请注意,这里的K是链表的数量,而不是链表的长度。在实际应用中,如果链表数量远小于链表长度,那么K可能会比N小得多,这样时间复杂度和空间复杂度都会相对较低。
五、总结知识点
-
链表(Linked List):代码中使用了单向链表的数据结构,每个
ListNode
包含一个值(val
)和一个指向下一个节点的指针(next
)。 -
优先队列(PriorityQueue):为了有效地合并多个已排序的链表,代码使用了Java的
PriorityQueue
,它是一个基于优先级堆(通常是最小堆)的无界队列。在这个场景中,它被用来存储所有链表的头节点,以便快速取出当前最小的节点。 -
比较器(Comparator):在创建
PriorityQueue
时,使用了自定义的比较器来定义节点之间的排序规则。这里通过比较节点的值(val
)来实现升序排列。 -
虚拟头节点(Dummy Node):为了避免在合并链表时处理特殊情况(如空链表),代码中创建了一个虚拟头节点
dummy
。这样可以简化代码逻辑,因为始终有一个有效的current
节点可以连接新的节点。 -
循环和条件判断:在合并链表的循环中,使用了
while
循环和if
条件判断来控制流程,确保在每次迭代中都取出最小节点,并将其正确地连接到合并后的链表中。 -
指针操作:在链表操作中,
current
指针被用来遍历和构建新的链表。每次循环中,current
都会移动到新添加的节点,以便为下一个节点的添加做准备。 -
返回值:最终,函数返回虚拟头节点的下一个节点,即合并后链表的实际头节点。这是链表操作中常见的技巧,用于简化边界条件的处理。
以上就是解决这个问题的详细步骤,希望能够为各位提供启发和帮助。