合并 K 个升序链表[困难]

一、题目

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

示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:[ 1->4->5, 1->3->4, 2->6 ]
将它们合并到一个有序链表中得到1->1->2->3->4->4->5->6

示例 2:
输入:lists = []
输出:[]

示例 3:
输入:lists = [[]]
输出:[]

k == lists.length
0 <= k <= 10^4
0 <= lists[i].length <= 500
-10^4 <= lists[i][j] <= 10^4
lists[i]按 升序 排列
lists[i].length的总和不超过10^4

二、代码

合并两个有序链表: 在解决「合并K个排序链表」这个问题之前,我们先来看一个更简单的问题:如何合并两个有序链表?假设链表ab的长度都是n,如何在O(n)的时间代价以及O(1)的空间代价完成合并? 这个问题在面试中常常出现,为了达到空间代价是O(1),我们的宗旨是「原地调整链表元素的next指针完成合并」。以下是合并的步骤和注意事项,对这个问题比较熟悉的读者可以跳过这一部分。此部分建议结合代码阅读。
【1】首先我们需要一个变量head来保存合并之后链表的头部,你可以把head设置为一个虚拟的头(也就是headval属性不保存任何值),这是为了方便代码的书写,在整个链表合并完之后,返回它的下一位置即可。
【2】我们需要一个指针tail来记录下一个插入位置的前一个位置,以及两个指针aPtrbPtr来记录ab未合并部分的第一位。注意这里的描述,tail不是下一个插入的位置,aPtrbPtr所指向的元素处于「待合并」的状态,也就是说它们还没有合并入最终的链表。 当然你也可以给他们赋予其他的定义,但是定义不同实现就会不同。
【3】当aPtrbPtr都不为空的时候,取val属性较小的合并;如果aPtr为空,则把整个bPtr以及后面的元素全部合并;bPtr为空时同理。
【4】在合并的时候,应该先调整tailnext属性,再后移tail*Ptr或者bPtr。那么这里tail*Ptr是否存在先后顺序呢?它们谁先动谁后动都是一样的,不会改变任何元素的next指针。

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;
}

时间复杂度: O(n)
空间复杂度: O(1)

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

class Solution {public ListNode mergeKLists(ListNode[] lists) {ListNode ans = null;for (int i = 0; i < lists.length; ++i) {ans = mergeTwoLists(ans, lists[i]);}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;}
}

时间复杂度: 假设每个链表的最长长度是n。在第一次合并后,ans的长度为n;第二次合并后,ans的长度为2×n,第i次合并后,ans的长度为i×n。第i次合并的时间代价是O(n+(i−1)×n)=O(i×n),那么总的时间代价为O(∑i=1k(i×n))=O(((1+k)⋅k)/2×n)=O(k^2n),故渐进时间复杂度为O(k^2 n)
空间复杂度: 没有用到与kn规模相关的辅助空间,故渐进空间复杂度为O(1)

【2】分治合并: 考虑优化方法一,用分治的方法进行合并。
1、将k个链表配对并将同一对中的链表合并;
2、第一轮合并以后,k个链表被合并成了k/2个链表,平均长度为2n/k​,然后是k/4个链表,k/8个链表等等;
3、重复这一过程,直到我们得到了最终的有序链表。

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/2组链表,每一组的时间代价是O(2n);第二轮合并k/4组链表,每一组的时间代价是O(4n)…所以总的时间代价是O(∑i=1∞k/2^i×2^in)=O(kn×log⁡k),故渐进时间复杂度为O(kn×log⁡k)
空间复杂度: 递归会使用到O(log⁡k)空间代价的栈空间。

【3】使用优先队列合并: 这个方法和前两种方法的思路有所不同,我们需要维护当前每个链表没有被合并的元素的最前面一个,k个链表就最多有k个满足这样条件的元素,每次在这些元素里面选取val属性最小的元素合并到答案中。在选取最小元素的时候,我们可以用优先队列来优化这个过程。

class Solution {class Status implements Comparable<Status> {int val;ListNode ptr;Status(int val, ListNode ptr) {this.val = val;this.ptr = ptr;}public int compareTo(Status status2) {return this.val - status2.val;}}PriorityQueue<Status> queue = new PriorityQueue<Status>();public ListNode mergeKLists(ListNode[] lists) {for (ListNode node: lists) {if (node != null) {queue.offer(new Status(node.val, 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.val, f.ptr.next));}}return head.next;}
}

时间复杂度: 考虑优先队列中的元素不超过k个,那么插入和删除的时间代价为O(log⁡k),这里最多有kn个点,对于每个点都被插入删除各一次,故总的时间代价即渐进时间复杂度为O(kn×log⁡k)
空间复杂度: 这里用了优先队列,优先队列中的元素不超过k个,故渐进空间复杂度为O(k)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/341639.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PDF-XChange Editor v10.2.0.384

软件介绍 PDF-XChange Editor&#xff0c;号称打开速度最快最强大的PDF编辑器/PDF阅读器&#xff0c;PDF-XChange专注于PDF文档的编辑&#xff0c;打开PDF文件速度快&#xff0c;软件小功能强大&#xff0c;可以自定义制作PDF电子文档&#xff0c;具有创建&#xff0c;查看&am…

文件与流(C++)

六、文件与流6.1 IO流库概念6.2 istream 与 ostream6.2.1 istream6.2.2 ostream6.2.3 输入输出的格式控制 6.3 string流6.4 文件流 六、文件与流 6.1 IO流库概念 C建立了一个十分庞大的流类库来实现数据的输入输 出操作。其中的每个流类实现不同的功能&#xff0c;这些类通过…

C++学习笔记(三十五):c++ 函数指针及lambda表达式

本节介绍c函数指针。在一些源码中经常能看到c函数指针&#xff0c;但之前一直觉着这一块比较复杂&#xff0c;就一直没去仔细研究&#xff0c;终于有时间去仔细研究这一块内容了。 c风格的函数指针 函数指针是指将一个函数赋值给一个变量的方法&#xff0c;可以将函数作为一个参…

linux命令--管道详解

关于linux命令的管道符的理解 管道&#xff1a;是进程之间的一种通信方式&#xff0c;主要适用的典型类型&#xff1a;父子进程 创建管道的函数c底层接口&#xff1a;int pipe(int f[2]) 返回的int类型&#xff0c;f[1]代表写&#xff0c;f[0]代表读 父进程使用fork函数创建副…

2024美赛数学建模思路 - 复盘:光照强度计算的优化模型

文章目录 0 赛题思路1 问题要求2 假设约定3 符号约定4 建立模型5 模型求解6 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 问题要求 现在已知一个教室长为15米&#xff0c;宽为12米&…

2024年1月11日 主题:非枪人生

2024年1月11日15:58:29 2024年1月11日15:35:13 2024年1月11日15:57:51 对物理进行大致预 2024年1月11日20:27:14 结论&#xff1a;不适合进行数据结构的训练和对电路的模拟感受 2024年1月11日20:28:32 今天也平静的结束了 不需要键盘的支持也就这么结束了我也不知道…

怎么处理网站的一些安全风险

随时互联网的持续发展&#xff0c;数字化转型步伐不断加快&#xff0c;社会各行业都走进了信息化、数字化。但与此同时&#xff0c;网络发展带来了许多风险&#xff0c;各行业面临着日益复杂的数据安全和网络安全威胁。其中&#xff0c;网站的安全风险持续增长&#xff0c;是各…

C++ Primer 6.2参数传递 知识点+练习题

C Primer 6.2参数传递 知识点练习题 指针形参使用引用拷贝Const 形参实参尽量使用常量引用数组形参数组引用形参传递多维数组向main函数传参数含有可变形参的函数练习题待更新 指针形参 void reset(int *p) {*p0;//p指向的整型对象变为0p0;//只是对形参改变p&#xff0c;使其为…

详解如何撰写一个基础的技术交底书

大家好,我是英子老师。作为一名知识产权专家,深耕于专利行业十余年,具有丰富的专利工作经验:曾在大型专利代理机构从事专利代理工作、专利质检工作(抽查代理机构的专利代理人的撰写质量并评分);之后在知名上市企业、行业龙头企业担任高级专利工程师的职位,主要工作内容…

Linux ----冯诺依曼体系结构与操作系统

目录 前言 一、冯诺依曼体系结构 二、为什么选择冯诺依曼体系结构&#xff1f; 三、使用冯诺依曼结构解释问题 问题1&#xff1a; 问题2: 四、操作系统 1.操作系统是什么 2.为什么需要操作系统 3.操作系统怎样管理的 4.如何给用户提供良好环境 五、我们是怎样调用系…

2023年全国职业院校技能大赛软件测试赛题—单元测试卷④

任务二 单元测试 一、任务要求 题目1&#xff1a;根据下列流程图编写程序实现相应分析处理并显示结果。返回结果“ax&#xff1a;”&#xff08;x为2、3或4&#xff09;&#xff1b;其中变量x、y均须为整型。编写程序代码&#xff0c;使用JUnit框架编写测试类对编写的程序代码…

第19课 在Android环境中使用FFmpeg和openCV进行开发的一般步骤

在上节课&#xff0c;根据模板文件我们对在Android环境中使用FFmpeg和openCV进行开发有了一个初步的体验&#xff0c;这节课&#xff0c;我们来具体看一下其工作流程。 1.程序的入口 与VS2013程序开发类似&#xff0c;Android程序开发也有一个入口&#xff0c;在这个模板中&a…