🚩传送门:牛客题目

小根堆创建:Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);

题目

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

前置知识:合并两个有序链表

如何在 O(n) 的时间代价以及 O(1) 的空间代价完成合并 ?

宗旨是原地调整链表元素的 next 指针完成合并

  1. 首先创建变量 head 来保存合并之后链表的头部,其指向新创建的头结点 (val 不保存任何值)
  2. 接着创建变量 tail 指向新链表的尾部
  3. aPtrbPtr 都不为空的时候,取 val 熟悉较小的合并;(二路归并
    • 如果 aPtr 为空,则把整个 bPtr 以及后面的元素全部合并
    • 如果 bPtr 为空,则把整个 aPtr 以及后面的元素全部合并

复杂度分析

时间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图1

空间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图2

我的代码

  1. public ListNode mergeTwoLists(ListNode a, ListNode b) {
  2. //1. 若空返回
  3. if (a == null || b == null) {
  4. return a != null ? a : b;
  5. }
  6. //2. 创建头结点
  7. ListNode head = new ListNode(0);
  8. //3. 初始化指针
  9. ListNode tail = head, aPtr = a, bPtr = b;
  10. //4. 若两个均不为空
  11. while (aPtr != null && bPtr != null) {
  12. //5. a 值小则并入
  13. if (aPtr.val < bPtr.val) {
  14. tail.next = aPtr;
  15. aPtr = aPtr.next;
  16. } else {
  17. tail.next = bPtr;
  18. bPtr = bPtr.next;
  19. }
  20. tail = tail.next;
  21. }
  22. //6. 将剩余不为空的并入
  23. tail.next = (aPtr != null ? aPtr : bPtr);
  24. return head.next;
  25. }

解题思路:顺序合并

我们可以想到一种最朴素的方法:用一个变量 ans 来维护以及合并的链表,第 i 次循环把第 i 个链表和 ans 合并,答案保存到 ans 中。

复杂度分析

时间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图3

假设每个链表的最长长度是 n。在第一次合并后,ans 的长度为 n;第二次合并后,ans 的长度为 2×n,第 i 次合并后,ans 的长度为 i×n。第 i 次合并的时间代价是 O(i×n),那么总的时间代价为🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图4,故渐进时间复杂度为 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图5

空间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图6

我的代码

  1. class Solution {
  2. public ListNode mergeKLists(ListNode[] lists) {
  3. ListNode ans = null;
  4. //1. 将所有的链表和 ans 合并
  5. for (int i = 0; i < lists.length; ++i) {
  6. ans = mergeTwoLists(ans, lists[i]);
  7. }
  8. //2. 返回合并后的 ans
  9. return ans;
  10. }
  11. public ListNode mergeTwoLists(ListNode a, ListNode b) {
  12. if (a == null || b == null) {
  13. return a != null ? a : b;
  14. }
  15. ListNode head = new ListNode(0);
  16. ListNode tail = head, aPtr = a, bPtr = b;
  17. while (aPtr != null && bPtr != null) {
  18. if (aPtr.val < bPtr.val) {
  19. tail.next = aPtr;
  20. aPtr = aPtr.next;
  21. } else {
  22. tail.next = bPtr;
  23. bPtr = bPtr.next;
  24. }
  25. tail = tail.next;
  26. }
  27. tail.next = (aPtr != null ? aPtr : bPtr);
  28. return head.next;
  29. }
  30. }

解题思路:分治合并

考虑优化方法,可以用分治的方法进行合并。

  1. k 个链表配对并将同一对中的链表合并;
  2. 第一轮合并以后, kk 个链表被合并成了 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图7 个链表,平均长度为 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图8 ,然后是 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图9 个链表, 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图10 个链表等
  3. 重复这一过程,直到我们得到了最终的有序链表。

🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图11

复杂度分析

时间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图12

考虑递归「向上回升」的过程——第一轮合并 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图13 组链表,每一组的时间代价是 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图14;第二轮合并 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图15 组链表,每一组的时间代价是 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图16……总的时间代价是 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图17

空间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图18,递归会使用到 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图19 空间代价的栈空间。

我的代码

  1. class Solution {
  2. public ListNode mergeKLists(ListNode[] lists) {
  3. return merge(lists, 0, lists.length - 1);
  4. }
  5. public ListNode merge(ListNode[] lists, int l, int r) {
  6. if (l == r) {
  7. return lists[l];
  8. }
  9. if (l > r) {
  10. return null;
  11. }
  12. int mid = (l + r) >> 1;
  13. return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
  14. }
  15. public ListNode mergeTwoLists(ListNode a, ListNode b) {
  16. if (a == null || b == null) {
  17. return a != null ? a : b;
  18. }
  19. ListNode head = new ListNode(0);
  20. ListNode tail = head, aPtr = a, bPtr = b;
  21. while (aPtr != null && bPtr != null) {
  22. if (aPtr.val < bPtr.val) {
  23. tail.next = aPtr;
  24. aPtr = aPtr.next;
  25. } else {
  26. tail.next = bPtr;
  27. bPtr = bPtr.next;
  28. }
  29. tail = tail.next;
  30. }
  31. tail.next = (aPtr != null ? aPtr : bPtr);
  32. return head.next;
  33. }
  34. }

解题思路:使用优先队列合并

  1. 用优先队列建一个小顶堆,每次堆顶为值最小的节点,依次取出,然后再将它的下一个节点放回去。
  2. 再重复过程

复杂度分析

时间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图20

考虑优先队列中的元素不超过 k 个,那么插入和删除的时间代价为 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图21,这里最多有 kn 个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为 🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图22

空间复杂度:🚩[NC]51. 合并k个已排序的链表 [创建小根堆方法] - 图23,这里用了优先队列,优先队列中的元素不超过 k 个。

我的代码

  1. public class Solution {
  2. public ListNode mergeKLists(ArrayList<ListNode> lists) {
  3. //1. 创建小根堆
  4. Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
  5. //2. 将全部的节点放进去
  6. for (ListNode node: lists) {
  7. if (node != null) {
  8. pq.offer(node);
  9. }
  10. }
  11. //3. 创建头结点
  12. ListNode dummyHead = new ListNode(0);
  13. ListNode tail = dummyHead;
  14. while (!pq.isEmpty()) {
  15. //4. 队首为值最小的节点
  16. ListNode minNode = pq.poll();
  17. tail.next = minNode;
  18. tail = minNode;
  19. //5. 若拿出来的节点的下一个节点不为null,则再将其放回去
  20. if (minNode.next != null) {
  21. pq.offer(minNode.next);
  22. }
  23. }
  24. //4. 返回新的头结点
  25. return dummyHead.next;
  26. }
  27. }