链表(4)_合并K个升序链表_面试题

ops/2024/10/18 22:30:37/

个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创

链表(4)_合并K个升序链表_面试

收录于专栏【经典算法练习】
本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论💌
 

目录

1. 题目链接

2. 题目描述

3. 解法

方法一: 暴力解法

算法思路:

代码展示: 

方法二: 利用优先级队列做优化

算法原理:

代码展示: 

方法三: 分治 - 递归

算法原理:

代码展示: 


1. 题目链接

OJ链接 : 合并K个升序链表 

2. 题目描述

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表

示例 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

3. 解法

方法一: 暴力解法

算法思路:

暴力算法很容易想到, 我们可以联想合并两个有序链表那道题, 那么有这道题的启发, 我们可以直接两两合并这K个升序链表.至于时间复杂度的分析, 我放到了代码展示的模块中, 说实话, 在面试中, 尽量不要写出暴力的方法, 一般通不过的~~

代码展示: 

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     ListNode *next;*     ListNode() : val(0), next(nullptr) {}*     ListNode(int x) : val(x), next(nullptr) {}*     ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {if(lists.empty()) return nullptr;ListNode* cur1 = lists[0];ListNode* head = new ListNode(0);for(int i = 1; i < lists.size(); i++){ListNode* cur2 = lists[i];ListNode* prev = head;while(cur1 || cur2){if(cur1 && cur2){if(cur1->val < cur2->val){prev->next = cur1;prev = cur1;cur1 = cur1->next;}else{prev->next = cur2;prev = cur2;cur2 = cur2->next;}}else{if(cur1){prev->next = cur1;prev = cur1;cur1 = cur1->next;}if(cur2){prev->next = cur2;prev = cur2;cur2 = cur2->next;}}}cur1 = head->next;head->next = nullptr;}delete head;return cur1;}
};

怎么说呢, 纯暴力, 没有技巧, 但是我万万没想到啊~, 居然过了~ 

但是, 这种题大概率会在面试题中遇到, 如果你给hr写出这样的代码, hr大概率会让你想想其他的方法(递归...), 反正就是要比暴力算法要好, 想不到的话, 可能直接让你回去等通知了(寄了)

那这个暴力算法真的有这么不堪吗? 别着急, 接下来我就详细分析一下它的时间复杂度. 

暴力算法时间复杂度分析:
合并两个链表的时间复杂度为 O(N),其中 N 是两个链表的总节点数。由于每次合并可能会处理 M 个节点。外层循环运行 K - 1 次(因为合并 K 个链表需要进行 K - 1 次合并),内层合并两个链表的操作总体上处理 M 个节点。因此,时间复杂度为:
O(M⋅K)其中 M 是所有链表节点的总数。

又因为我们的链表在合并的过程中是递增的, 假如每个链表长度固定为n, 那我们每合并一次, 我们的链表长度就会增加n, 也就是说我们合并时链表增长为: n, 2n, 3n, 4n, ... kn , 所以我们总的时间复杂度为: (n + kn)(k - 1)/2

总的时间复杂度为: O(k^2*n)

假如说n ~= k 的话, 那我们暴力算法的时间复杂度为O(n ^ 3) , 非常恐怖, 这道题我们能够通过的原因在于n < 10^2 

方法二: 利用优先级队列做优化

算法原理:

合并两个有序链表是比较简单的, 就是使用双指针一次比较链表1, 链表2未排序的最小元素, 选择更小的哪一个加入有序的答案链表中.

合并k个链表时, 我们依旧可以选择k个链表中, 头节点值最小的哪一个, 那么如何快速的得到头节点最小的是哪一个呢? 用堆这个数据结构就好了~

我们可以把所有的头节点放进一个小根堆中, 这样就能快速找到每次k个链表中, 最小的元素是那个. 

代码展示: 

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     ListNode *next;*     ListNode() : val(0), next(nullptr) {}*     ListNode(int x) : val(x), next(nullptr) {}*     ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {struct cmp{bool operator()(const ListNode* l1, const ListNode* l2){return l1->val > l2->val;}};public:ListNode* mergeKLists(vector<ListNode*>& lists) {priority_queue<ListNode*, vector<ListNode*>, cmp> heap;for(auto l : lists)if(l) heap.push(l);ListNode* head = new ListNode(0);ListNode* prev = head;while(!heap.empty()){ListNode* t = heap.top();heap.pop();prev->next = t;prev = t;if(t->next) heap.push(t->next);}prev = head->next;delete head;return prev;}
};

时间复杂度分析
空间复杂度:O(K),用于存储 K 个链表的头节点在堆中。

时间复杂度:
将 K 个头节点插入堆:O(K* log K)。
每个节点的处理(假设总共 N 个节点):O(N* log K),因为每次取出最小值和插入下一个节点都涉及到堆的操作。
因此,整体时间复杂度为 O(N log K),其中 N 是所有链表中节点的总数,K 是链表的数量。 

假设每个链表的节点为n, 所有链表的节点总数为: kn, 即总的时间复杂度为 : O(n * k * logk) 

方法三: 分治 - 递归

算法原理:

逐一比较时,答案链表越来越长,每个跟它合并的小链表的元素都需要比较很多次才可以成功排序。
比如,我们有 8 个链表,每个链表长为 100。
逐一合并时,我们合并链表的长度分别为(0, 100), (100, 100), (200, 100), (300, 100), (400, 100), (500, 100), (600, 100), (700, 100)。所有链表的总长度共计 3600。
    如果尽可能让长度相同的链表进行两两合并呢?这时合并链表的长度分别是(100, 100) x 4, (200, 200) x 2, (400, 400),共计 2400。比上⼀种的计算量整整少了 1 / 3。
    迭代的做法代码细节会稍多一些, 这里还是推荐方法二, 不过还是怕面试中, 会被问到~~

算法流程:

1. 特判,如果题目给出空链表,无需合并,直接返回;
2. 返回递归结果。
递归函数设计:
1. 递归出口:如果当前要合并的链表编号范围左右值相等,无需合并,直接返回当前链表
2. 应用二分思想,等额划分左右两段需要合并的链表,使这两段合并后的长度尽可能相等;
3. 对左右两段分别递归,合并[l, r]范围内的链表
4. 再调用 mergeTwoLists 函数进行合并(就是合并两个有序链表

代码展示: 

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     ListNode *next;*     ListNode() : val(0), next(nullptr) {}*     ListNode(int x) : val(x), next(nullptr) {}*     ListNode(int x, ListNode *next) : val(x), next(next) {}* };*/
class Solution {
public:ListNode* mergeKLists(vector<ListNode*>& lists) {return merge(lists, 0, lists.size() - 1);}ListNode* merge(vector<ListNode*>& lists, int left, int right){if(left > right) return nullptr;if(left == right) return lists[left];int mid = (left + right) >> 1;ListNode* l1 = merge(lists, left, mid); ListNode* l2 = merge(lists, mid + 1, right);return mergecombine(l1, l2);}ListNode* mergecombine(ListNode* l1, ListNode* l2){if(l1 == nullptr) return l2;if(l2 == nullptr) return l1;ListNode* head = new ListNode(0);ListNode* prev = head, *cur1 = l1, *cur2 = l2;while(cur1 && cur2){if(cur1->val < cur2->val) {prev = prev->next = cur1;cur1 = cur1->next;}else{prev = prev->next = cur2;cur2 = cur2->next;}}if(cur1) prev->next = cur1;if(cur2) prev->next = cur2;prev = head->next;delete head;return prev;}
};

 

时间复杂度分析
分治递归的时间复杂度:

每次递归都将链表数组分成两半,所以递归的深度为 O(log K),其中 K 是链表的数量。
在每一层的递归中,需要合并 K 个链表中的部分,最多需要遍历每个链表的所有节点。假设所有链表的节点数总和为 N,则合并的时间复杂度为 O(N)。
因此,总体时间复杂度为 O(N log K),其中 N 是所有链表中的节点数,K 是链表的数量。

假设每个链表的节点为n, 所有链表的节点总数为: kn, 即总的时间复杂度为 : O(n * k * logk) 
空间复杂度:

由于递归栈的深度为 O(log K),所以空间复杂度是 O(log K),这是递归分治的开销。
合并过程中的额外空间是常数级别的,因此整体空间复杂度为 O(log K)。


http://www.ppmy.cn/ops/124882.html

相关文章

python不用ide也能进行调试

import pdb pdb.set_trace()import pdb 和 pdb.set_trace() 是 Python 中用于调试代码的工具。以下是它们的具体含义和用法&#xff1a; import pdb pdb 是 Python 的内置调试器模块&#xff0c;允许开发者在运行时进行代码调试。 通过 import pdb 语句&#xff0c;你可以引入…

智能手机、平板和笔记本电脑出口俄罗斯认证解析

智能手机、笔记本电脑和平板电脑&#xff0c;它们的监管范围相似&#xff0c;需要获得EAC 合格证、FAC电信认证和FSS加密认证&#xff0c;才能进口、清关并在俄罗斯市场上销售。 一、海关联盟EAC 认证 是根据 EAC 要求强制批准的证书&#xff0c;并且受到所有国家海关和市场的…

2024高校网络安全管理运维赛 wp

0x00 前言 本文是关于“2024高校网络安全管理运维赛”的详细题解&#xff0c;主要针对Web、Pwn、Re、Misc以及Algorithm等多方向题目的解题过程&#xff0c;包含但不限于钓鱼邮件识别、流量分析、SQLite文件解析、ssrf、xxe等等。如有错误&#xff0c;欢迎指正。 0x01 Misc 签到…

LeetCode讲解篇之300. 最长递增子序列

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 这题我们可以通过动态规划求解&#xff0c;使用一个数组f&#xff0c;数组f的i号元素表示[0, i]范围内最长递增子序列的长度 状态转移方程&#xff1a;f[i] max(f[j] 1)&#xff0c;其中 0 < j < i 题…

尚硅谷rabbitmq 2024 流式队列2024指定偏移量 第55节答疑

rabbitmq的stream&#xff1a; 4、对比 autoTrackingstrategy方式:始终监听Stream中的新消息(狗狗看家&#xff0c;忠于职守)指定偏移量方式:针对指定偏移量的消息消费之后就停止(狗狗叼飞盘&#xff0c;回来就完) 这两种分别怎么写&#xff1f;java 在 RabbitMQ 中&#xff0c…

利用 Pgpool-II 实现 IvorySQL 集群读写分离

读写分离是数据库架构中的一种常见策略&#xff0c;它通过将读操作和写操作分开处理来优化数据库的负载&#xff0c;可以显著提高数据库系统的性能和可伸缩性。 Pgpool-II 是一个强大的 PostgreSQL 中间层代理&#xff0c;能够为PostgreSQL 集群提供读写分离、负载均衡等功能。…

企业级im架构详解, 分布式im 荔枝im即时消息分享 即时通讯im

1. 最近看了荔枝集团在infoQ 的im技术分享视频&#xff0c;干货满满。52im好像没有看到这个分享 2. b站链接&#xff1a; IM即时消息系统在荔枝的落地实践_哔哩哔哩_bilibili 3. infoQ 链接: IM即时消息系统在荔枝的落地实践 | InfoQ 公开课_架构_郑思宇_InfoQ精选视频 4. …

五款专业三维数据处理工具:GISBox、Cesiumlab、OSGBLab、灵易智模、倾斜伴侣深度解析

随着三维数据处理技术的广泛应用&#xff0c;尤其是在城市规划、地理信息系统&#xff08;GIS&#xff09;、工程监测等领域&#xff0c;处理倾斜摄影、三维建模以及大规模数据管理的需求日益增加。以下是五款我精心挑选的倾斜摄影和三维数据处理工具——GISBox、Cesiumlab、OS…