🚩传送门:牛客题目
小根堆创建:
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
题目
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
前置知识:合并两个有序链表
如何在 O(n) 的时间代价以及 O(1) 的空间代价完成合并 ?
宗旨是「原地调整链表元素的 next 指针完成合并」
- 首先创建变量 head 来保存合并之后链表的头部,其指向新创建的头结点 (val 不保存任何值)
- 接着创建变量 tail 指向新链表的尾部
- 当 aPtr 和 bPtr 都不为空的时候,取 val 熟悉较小的合并;(二路归并)
- 如果 aPtr 为空,则把整个 bPtr 以及后面的元素全部合并
- 如果 bPtr 为空,则把整个 aPtr 以及后面的元素全部合并
复杂度分析
时间复杂度:
空间复杂度:
我的代码
public ListNode mergeTwoLists(ListNode a, ListNode b) {
//1. 若空返回
if (a == null || b == null) {
return a != null ? a : b;
}
//2. 创建头结点
ListNode head = new ListNode(0);
//3. 初始化指针
ListNode tail = head, aPtr = a, bPtr = b;
//4. 若两个均不为空
while (aPtr != null && bPtr != null) {
//5. a 值小则并入
if (aPtr.val < bPtr.val) {
tail.next = aPtr;
aPtr = aPtr.next;
} else {
tail.next = bPtr;
bPtr = bPtr.next;
}
tail = tail.next;
}
//6. 将剩余不为空的并入
tail.next = (aPtr != null ? aPtr : bPtr);
return head.next;
}
解题思路:顺序合并
我们可以想到一种最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。
复杂度分析
时间复杂度:
假设每个链表的最长长度是 n。在第一次合并后,ans 的长度为 n;第二次合并后,ans 的长度为 2×n,第 i 次合并后,ans 的长度为 i×n。第 i 次合并的时间代价是 O(i×n),那么总的时间代价为,故渐进时间复杂度为
。
空间复杂度:
我的代码
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode ans = null;
//1. 将所有的链表和 ans 合并
for (int i = 0; i < lists.length; ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
//2. 返回合并后的 ans
return ans;
}
public ListNode mergeTwoLists(ListNode a, ListNode b) {
if (a == null || b == null) {
return a != null ? a : b;
}
ListNode head = new ListNode(0);
ListNode tail = head, aPtr = a, bPtr = b;
while (aPtr != null && bPtr != null) {
if (aPtr.val < bPtr.val) {
tail.next = aPtr;
aPtr = aPtr.next;
} else {
tail.next = bPtr;
bPtr = bPtr.next;
}
tail = tail.next;
}
tail.next = (aPtr != null ? aPtr : bPtr);
return head.next;
}
}
解题思路:分治合并
考虑优化方法,可以用分治的方法进行合并。
- 将 k 个链表配对并将同一对中的链表合并;
- 第一轮合并以后, kk 个链表被合并成了
个链表,平均长度为
,然后是
个链表,
个链表等
- 重复这一过程,直到我们得到了最终的有序链表。
复杂度分析
时间复杂度:
考虑递归「向上回升」的过程——第一轮合并 组链表,每一组的时间代价是
;第二轮合并
组链表,每一组的时间代价是
……总的时间代价是
空间复杂度:,递归会使用到
空间代价的栈空间。
我的代码
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
public ListNode merge(ListNode[] lists, int l, int r) {
if (l == r) {
return lists[l];
}
if (l > r) {
return null;
}
int mid = (l + r) >> 1;
return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
}
public ListNode mergeTwoLists(ListNode a, ListNode b) {
if (a == null || b == null) {
return a != null ? a : b;
}
ListNode head = new ListNode(0);
ListNode tail = head, aPtr = a, bPtr = b;
while (aPtr != null && bPtr != null) {
if (aPtr.val < bPtr.val) {
tail.next = aPtr;
aPtr = aPtr.next;
} else {
tail.next = bPtr;
bPtr = bPtr.next;
}
tail = tail.next;
}
tail.next = (aPtr != null ? aPtr : bPtr);
return head.next;
}
}
解题思路:使用优先队列合并
- 用优先队列建一个小顶堆,每次堆顶为值最小的节点,依次取出,然后再将它的下一个节点放回去。
- 再重复过程
复杂度分析
时间复杂度:
考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 ,这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为
。
空间复杂度:,这里用了优先队列,优先队列中的元素不超过 k 个。
我的代码
public class Solution {
public ListNode mergeKLists(ArrayList<ListNode> lists) {
//1. 创建小根堆
Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
//2. 将全部的节点放进去
for (ListNode node: lists) {
if (node != null) {
pq.offer(node);
}
}
//3. 创建头结点
ListNode dummyHead = new ListNode(0);
ListNode tail = dummyHead;
while (!pq.isEmpty()) {
//4. 队首为值最小的节点
ListNode minNode = pq.poll();
tail.next = minNode;
tail = minNode;
//5. 若拿出来的节点的下一个节点不为null,则再将其放回去
if (minNode.next != null) {
pq.offer(minNode.next);
}
}
//4. 返回新的头结点
return dummyHead.next;
}
}