🚩传送门:牛客题目
小根堆创建:
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. 返回合并后的 ansreturn 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;}}
