题目描述:
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
一、基础类
public static class ListNode {
int val;
ListNode next;
ListNode() {
}
ListNode(int val) {
this.val = val;
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
二、官方解答
方法一、顺序合并
思路与算法
我们可以想到一种最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。
代码
ListNode mergeKLists(ListNode[] lists) {
ListNode ans = null;
for (ListNode list : lists) {
ans = mergeTwoLists(ans, list);
}
return ans;
}
// 这里利用了上篇文章的方法,进阶版
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.value < bPtr.value) {
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;
}
复杂度分析
- 时间复杂度:假设每个链表的最长长度是 n。在第一次合并后,ans 的长度为 n;第二次合并后,ans 的长度为 2×n,第 i 次合并后,ans 的长度为 i×n。第 i 次合并的时间代价是 O(n+(i−1)×n)=O(i×n),那么总的时间代价为 O(k^2 n),故渐进时间复杂度为 O(k^2 n)。
- 空间复杂度:没有用到与 k 和 n 规模相关的辅助空间,故渐进空间复杂度为 O(1)。
方法二、分治合并
思路与算法
考虑优化方法一,用分治的方法进行合并。
将 k 个链表配对并将同一对中的链表合并;
第一轮合并以后, k 个链表被合并成了 k / 2 个链表,平均长度为 2n / k ,然后是 k / 4 个链表,k / 8 个链表等等;
重复这一过程,直到我们得到了最终的有序链表。
代码
ListNode mergeKLists2(ListNode[] lists) {
return merge2(lists, 0, lists.length - 1);
}
ListNode merge2(ListNode[] lists, int l, int r) {
if (l == r) return lists[l];
if (l > r) return null;
int mid = (l + r) >> 1;
return mergeTwoLists(merge2(lists, l, mid), merge2(lists, mid + 1, r));
}
复杂度分析
- 时间复杂度:考虑递归「向上回升」的过程——第一轮合并 k / 2 组链表,每一组的时间代价是 O(2n);第二轮合并 k / 4 组链表,每一组的时间代价是 O(4n)......所以总的时间代价是 O(kn×logk),故渐进时间复杂度为 O(kn×logk)。
- 空间复杂度:递归会使用到 O(logk) 空间代价的栈空间。
方法三、使用优先队列合并
思路与算法
嘻嘻,我原本想法是用堆排,即用堆排实现的现有结构优先队列更优
这个方法和前两种方法的思路有所不同,我们需要维护当前每个链表没有被合并的元素的最前面一个,k 个链表就最多有 k 个满足这样条件的元素,每次在这些元素里面选取 val 属性最小的元素合并到答案中。在选取最小元素的时候,我们可以用优先队列来优化这个过程。
代码
static class Status implements Comparable<Status> {
int val;
ListNode ptr;
Status(int val, ListNode ptr) {
this.val = val;
this.ptr = ptr;
}
public int compareTo(Status status) {
return this.val - status.val;
}
}
PriorityQueue<Status> queue = new PriorityQueue<>();
ListNode mergeKLists3(ListNode[] lists){
for (ListNode node : lists) {
if (node != null) {
queue.offer(new Status(node.value, node));
}
}
ListNode head = new ListNode(0);
ListNode tail = head;
while (!queue.isEmpty()) {
Status f = queue.poll();
tail.next = f.ptr;
tail = tail.next;
if (f.ptr.next != null) {
queue.offer(new Status(f.ptr.next.value, f.ptr.next));
}
}
return head.next;
}
复杂度分析
- 时间复杂度:考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 O(logk),这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 O(kn×logk)。
- 空间复杂度:这里用了优先队列,优先队列中的元素不超过 k 个,故渐进空间复杂度为 O(k)。
魔都日景