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

news2024/10/24 9:59:03

个人主页: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.coloradmin.cn/o/2210888.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第十五届蓝桥杯C++B组省赛

文章目录 1.握手问题解题思路1&#xff08;组合数学&#xff09;解题思路2&#xff08;暴力枚举&#xff09; 2.小球反弹做题思路 3.好数算法思路&#xff08;暴力解法&#xff09;---不会超时 4.R格式算法思路 5.宝石组合算法思路---唯一分解定理 6.数字接龙算法思路----DFS 7…

【Oracle数据库进阶】001.SQL基础查询_查询语句

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

Egg考古系列-EggCore的生命周期

关于EGG egg框架的第一个版本还是2017-03-21&#xff0c;距今已有7年了。虽然最近几年没有什么更新&#xff0c;但它在国内的使用还是挺多的&#xff0c;mvc的分层模式也很受大家喜欢。虽然声称是面向企业级、中大型项目场景的框架&#xff0c;但这种约定式在大型项目中其实也很…

高校学科竞赛管理:SpringBoot实现的高效策略

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

【M2-Mixer】核心方法解读

abstract&#xff1a; 在本文中&#xff0c;我们提出了M2-Mixer&#xff0c;这是一种基于MLPMixer的结构&#xff0c;具有多头损失&#xff0c;用于多模态分类。它比基于卷积、循环或神经结构搜索的基线模型具有更好的性能&#xff0c;其主要优势是概念和计算简单。所提出的多…

【电子电力】LCL滤波器设计,包括电流控制调谐

摘要 LCL 滤波器是电力电子领域中广泛应用于并网逆变器的滤波器之一&#xff0c;其主要功能是减少高频开关的谐波&#xff0c;确保输出电流的质量。本文设计并实现了基于 MATLAB 的 LCL 滤波器模型&#xff0c;结合电流控制器和调谐技术&#xff0c;验证了其在谐波抑制方面的效…

从RNN讲起(RNN、LSTM、GRU、BiGRU)——序列数据处理网络

文章目录 RNN&#xff08;Recurrent Neural Network&#xff0c;循环神经网络&#xff09;1. 什么是RNN&#xff1f;2. 经典RNN的结构3. RNN的主要特点4. RNN存在问题——长期依赖&#xff08;Long-TermDependencies&#xff09;问题 LSTM&#xff08;Long Short-Term Memory&a…

PostgreSQL学习笔记七:常规SQL操作

PostgreSQL 支持标准的 SQL 语句&#xff0c;同时也扩展了一些特有的功能。以下是一些常规的 SQL 语句示例&#xff0c;这些示例涵盖了数据定义、数据操作和数据查询的基本操作&#xff1a; 数据定义语言 (DDL 创建数据库&#xff1a; CREATE DATABASE mydatabase;创建表&#…

stm32单片机个人学习笔记9(TIM输入捕获)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

AWD入门

一、简介 AWD(Attack With Defense&#xff0c;攻防兼备)模式。你需要在一场比赛里要扮演攻击方和防守方&#xff0c;攻者得分&#xff0c;失守者会被扣分。也就是说攻击别人的靶机可以获取 Flag 分数时&#xff0c;别人会被扣分&#xff0c;同时你也要保护自己的主机不被别人…

Docker 教程四 (Docker 镜像加速)

Docker 镜像加速 国内从 DockerHub 拉取镜像有时会遇到困难&#xff0c;此时可以配置镜像加速器。 目前国内 Docker 镜像源出现了一些问题&#xff0c;基本不能用了&#xff0c;后期能用我再更新下。* Docker 官方和国内很多云服务商都提供了国内加速器服务&#xff0c;例如…

Python网络爬虫入门指南

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

MPA-SVM多变量回归预测|海洋捕食者优化算法-支持向量机|Matalb

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、算法介绍&#xff1a; 四、完整程序下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matlab平台编译&am…

2024年网络安全进阶学习路径-2024年进阶学习指南

&#x1f91f; 基于入门网络安全/黑客打造的&#xff1a;&#x1f449;黑客&网络安全入门&进阶学习资源包 前言 什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、…

KDD 2024论文分享┆用于序列推荐的数据集再生

论文简介 本推文介绍了2024 KDD的最佳学生论文《Dataset Regeneration for Sequential Recommendation》。该论文提出了一种基于数据中心化范式的新框架&#xff0c;称为DR4SR&#xff0c;该框架通过模型无关的数据再生机制&#xff0c;能够生成具有出色跨架构泛化能力的理想训…

git(版本回退,分支管理,vscode集成git)

一、安装与简单命令 1.官网 https://git-scm.com/downloads 2.查看版本号git --version 3.设置用户签名&#xff08;用户名和邮箱&#xff09; 用来标识用户&#xff0c;以区分不同的开发人员 git config --global user.name "Your Name" git config --global u…

2024年最新算法:青蒿素优化算法(Artemisinin Optimization Algorithm, AOA)原理介绍

青蒿素优化算法&#xff08;Artemisinin Optimization Algorithm, AOA&#xff09;是2024年提出的一种受青蒿素抗疟疾特性启发的元启发式优化算法。青蒿素是一种从中草药青蒿中提取的化合物&#xff0c;因其在治疗疟疾方面的显著效果而闻名。AOA算法的设计者将青蒿素的这一特性…

【机器学习】深入浅出讲解贝叶斯分类算法

0. 前言 1.贝叶斯分类器介绍 贝叶斯分类是一类分类算法的总称&#xff0c;这类算法均以贝叶斯定理为基础&#xff0c;故统称为贝叶斯分类。而朴素贝叶斯&#xff08;Naive Bayes&#xff09;分类是贝叶斯分类中最简单&#xff0c;也是常见的一种分类方法。 一些很常见的分类…

动态规划最大子段和讲解和【题解】——最大子段和

动态规划最大子段和讲解和【题解】——最大子段和 1.详细讲解最大子段和题目描述输入格式输出格式输入输出样例输入 #1输出 #1 提示样例 1 解释数据规模与约定 1.1.思路解析1.2.AC代码 2.优化3.别言 1.详细讲解 最大子段和 题目描述 给出一个长度为 n n n 的序列 a a a&am…

cursor: mutex X 等待事件分析

背景&#xff1a; v$session中同一个sql语句bhaku1zp2w5v7大量等待cursor: mutex X &#xff0c;且等待事件较长。 分析&#xff1a; 什么是cursor: mutex X&#xff1f; 任何操作或访问游标的操作都可能需要等待访问共享池中支持游标的结构。在极端争用的情况下&#xff0c…