当前位置: 首页 > news >正文

C++ 面试高频考点 链表 优先级队列 递归 力扣 23. 合并 K 个升序链表

文章目录

  • 零、题目描述
  • 一、为什么这道题值得你花时间弄懂?
  • 二、算法原理
    • 暴力解法(基于两链表合并)
      • 时间复杂度分析
    • 优化算法1:小堆实现的优先级队列
      • 时间复杂度分析
    • 优化算法2:分治-递归
      • 时间复杂度
  • 三、代码实现
    • 暴力解法
    • 优先级队列(小顶堆)解法
    • 分治归并解法
  • 四、总结
    • 三种解法对比(表格)
    • 关键考点与避坑点
    • 核心思想迁移
  • 五、下题预告

在这里插入图片描述
在这里插入图片描述

零、题目描述

题目链接:力扣 23. 合并 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 <= 104
0 <= lists[i].length <= 500,
-104 <= lists[i][j] <= 104
lists[i] 按 升序 排列,
lists[i].length 的总和不超过 104

一、为什么这道题值得你花时间弄懂?

“合并K个升序链表”不仅是算法优化的经典题,更是高频面试核心考点,常作为中难度必考题出现。

题目核心价值
这道题之所以成为面试“常客”,核心是它能一次性考察多项关键能力,完美匹配企业对算法工程师的要求:

  • 完整的优化链条:从暴力解法(O(nk²))到优先级队列优化(O(n k log k)),再到分治归并(O(n k log k)),每一步优化都对应面试中“如何逐步降低时间/空间复杂度”的核心提问方向,面试官常通过此追问“你为什么想到用堆/分治”“不同解法的优劣对比”;
  • 多场景算法思维迁移:优先级队列的应用(解决多数据源有序合并)、分治思想的落地(大问题拆解为小问题),都是面试中“算法迁移能力”的高频考察点,面试官会延伸提问“如何用类似思路解决TOP K、分布式排序”等问题;
  • 边界与细节处理能力:空链表判断、链表节点指针操作、优先级队列自定义比较器(避免节点值相同时的冲突)、递归终止条件设计,这些细节正是面试中区分“能写代码”和“能写稳健代码”的关键,也是高频丢分点;
  • 复杂度分析硬实力:这道题思路不是很难但是在时间复杂度分析上是个难点,面试中必然会要求分析每种解法的时间/空间复杂度,尤其是“为什么堆优化的时间复杂度是O(n k log k)”“分治归并如何优化空间复杂度”,这是考察基础算法素养的核心环节。

面试考察的核心方向

  1. 能否快速给出解法(如:堆/分治),并与暴力对比差异。
  2. 面对边界 case(如k=0、k=1、部分链表为空)时,代码是否严谨。
  3. 能否手动实现优先级队列的自定义比较规则。
  4. 能否解释分治归并的递归逻辑,或转化为迭代实现(避免递归栈溢出)。
  5. 能否延伸思考“如果链表是降序的该如何调整”“如何处理超大规模数据(无法一次性加载到内存)”等拓展问题。

掌握这道题,相当于覆盖了面试中“链表操作+算法优化+数据结构应用+复杂度分析”的多个核心考点,是提升面试通过率的关键突破口。

二、算法原理

暴力解法(基于两链表合并)

暴力解法的核心思路特别直接——靠“合并两个有序链表”的基础操作,多轮叠加后凑出最终结果

具体步骤:

  1. 先搞个空链表 ans 当“临时中转站”,存每次合并后的结果。
  2. 对着 K 个链表挨个下手,每次都把当前链表和 ans 合并,合并完就更新 ans,让它变成新的“半成品”。
  3. 等所有链表都遍历完,ans 就是咱们要的全量有序链表啦。

时间复杂度分析

结论:O(n*k²)
分析:
双指针合并两个有序链表的时间复杂度是 O(n+m)(n、m 分别是两个链表的长度),但这道题里 ans 是越合并越长的,咱们用图一看就懂👇
在这里插入图片描述
方便理解咱们简化下计算:设链表总数为 k,每个链表的平均长度为 n
多次合并的时间复杂度会变成 O(n + 2n + 3n + … + kn),最终折算下来就是 O(nk²),详细推导过程看这张图👇
在这里插入图片描述
说实在的,这复杂度差不多能当成 O(n³) 看了,效率是真不高!也就题目里有“所有链表长度总和不超过 10⁴”的约束,才能勉强跑通。
但面试场上要是只写这一种方法,估计就得和面试官大眼瞪小眼,最后喜提一封感谢信啦~

优化算法1:小堆实现的优先级队列

这种思路和两个链表合并有共通之处,但核心差异在于对比逻辑:两个链表合并可通过双指针直接对比,而k个链表若用双指针遍历,会导致时间复杂度飙升,重回暴力算法的困境。那么什么能够自动快速的找出带着最小值的节点呢?此时优先级队列的优势就尤为突出:它能自动、快速筛选出当前最小值节点。

详细的说就是我们一层一层来看,如下图👇:
在这里插入图片描述
结合上图,具体实现逻辑很清晰:

  1. 先将k个链表的第一个节点全部放入小堆中;
  2. 弹出堆顶的最小值节点,将其接入结果链表;
  3. 若弹出节点的下一个节点存在,则将该节点加入小堆;
  4. 重复“弹出-接入-入堆”的步骤,直到所有节点处理完毕。

时间复杂度分析

关键点又来了,那么这个方法的时间复杂度是多少呢我们来一起分析。
结论:O(nklogk)
分析:
小堆的单次查找的时间复杂度是logK,然而我们要修改nk次,如下图👇:
在这里插入图片描述
所以时间复杂度是:**O(n
k*logk)**

作者注:到这里大家可以先停下来,尝试自己实现这种方法,之后再对照后面的代码参考。亲手实践一遍,对提升代码能力会很有帮助~

优化算法2:分治-递归

归并排序的核心是“分治思想”,将K个链表拆分成两两一组,通过递归不断合并相邻的两个链表,最终得到一个完整的升序链表,同样能实现O(n log k)的时间复杂度,且空间复杂度(递归栈除外)比优先级队列更优。

作者注:这个方法的思路与归并排序几乎相同,无非就是将单个元素从数组中的数换成了节点,如果对归并排序不熟悉的朋友可以看我的这篇博客,其中详细讲解了归并排序的实现原理与思路 归并排序 力扣 912. 排序数组。

思路拆解

  • 定义递归函数 merge(l, r),表示合并 lists 中从索引 lr 的所有链表,返回合并后的头节点;
  • 递归终止条件:若 l == r,则直接返回 lists[l](只有一个链表,无需合并);若 l > r,返回空指针;
  • 递归拆分:计算中间索引 mid = (l + r) / 2,分别递归合并左半部分 merge(l, mid) 和右半部分 merge(mid + 1, r),得到两个升序链表 leftright
  • 合并子结果:调用“合并两个有序链表”的函数,将 leftright 合并为一个新的升序链表,返回该链表头节点;
  • 最终调用 merge(0, k - 1)(k为 lists 长度),得到合并后的完整链表。

时间复杂度

接下来我们详细讨论这个方法的时间复杂度:
结论:O(nklogk)
分析:
我们在合并的时候如图所示👇 (每一个方块代表一整个链表)
在这里插入图片描述
我们一共有K个链表在归并排序的合并层数就是logK,而每一层一次合并的时间复杂度就是一个链表的长度O(n),并且每一层有K个链表要合并,所以约等于O(nklogk)

三、代码实现

暴力解法

/*** 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* mergeTwoLists(ListNode* a, ListNode* b) {// 边界处理:其中一个链表为空时,直接返回另一个(无需合并)if (!a) return b;if (!b) return a;ListNode dummy;  // 哨兵节点:避免单独处理头节点,简化链表拼接逻辑ListNode* cur = &dummy;  // 游标指针:指向当前合并链表的尾部,用于接入新节点// 双指针遍历两个链表,直到其中一个遍历完毕while (a && b) {// 选择值较小的节点接入合并链表if (a->val < b->val) {cur->next = a;a = a->next;  // a链表指针后移,继续比较下一个节点} else {cur->next = b;b = b->next;  // b链表指针后移,继续比较下一个节点}cur = cur->next;  // 合并链表尾部指针后移,为下一次接入做准备}// 处理剩余节点:其中一个链表已遍历完,直接接入另一个链表的剩余部分cur->next = a ? a : b;return dummy.next;  // 哨兵节点的next即为合并后的有效头节点}// 主函数:合并k个升序链表,返回整体升序链表ListNode* mergeKLists(vector<ListNode*>& lists) {ListNode* ans = nullptr;  // 初始化结果链表为null(无节点状态)// 迭代合并:将每个链表依次与当前结果合并,更新结果链表for (ListNode* list : lists) {ans = mergeTwoLists(ans, list);}return ans;  // 最终合并后的升序链表}
};

优先级队列(小顶堆)解法

/*** 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:// 自定义优先级队列比较规则:实现小顶堆(值小的节点优先级高)// 注意:C++优先队列默认是大顶堆,需通过仿函数反向定义比较逻辑struct CompareNode {bool operator()(ListNode* a, ListNode* b) {return a->val > b->val;  // 大顶堆转小顶堆:a.val > b.val时,b优先出堆}};ListNode* mergeKLists(vector<ListNode*>& lists) {ListNode* head = new ListNode(-1);  // 哨兵节点:简化结果链表的头节点处理ListNode* tmp = head;  // 游标指针:用于拼接结果链表的节点// 定义小顶堆:存储ListNode*,底层容器为vector,比较规则为CompareNodepriority_queue<ListNode*, vector<ListNode*>, CompareNode> minHeap;// 初始化堆:将每个非空链表的头节点入堆(避免空指针入堆导致报错)for (ListNode* list : lists) {if (list != nullptr) {minHeap.push(list);}}// 循环处理堆中节点,直到堆为空(所有节点均已处理)while (!minHeap.empty()) {ListNode* curr = minHeap.top();  // 取出堆顶节点(当前所有候选节点中的最小值)minHeap.pop();  // 弹出堆顶节点,避免重复处理// 将当前最小节点拼接至结果链表tmp->next = curr;tmp = tmp->next;  // 游标后移,准备接收下一个节点// 若当前节点有后继节点,将后继节点入堆(补充候选节点池)if (curr->next != nullptr) {minHeap.push(curr->next);}}ListNode* result = head->next;  // 跳过哨兵节点,获取有效结果头节点delete head;  // 释放哨兵节点内存,避免内存泄漏return result;}
};

分治归并解法

/*** 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) {// 调用Msort,合并lists中[0, lists.size()-1]区间的所有链表return Msort(lists, 0, lists.size() - 1);}// 分治函数:递归合并lists中[left, right]区间的链表,返回合并后的头节点ListNode* Msort(vector<ListNode*>& lists, int left, int right) {if (right < left)  // 边界条件1:区间无效(无链表可合并),返回nullreturn nullptr;if (right == left)  // 边界条件2:区间内只有一个链表,直接返回该链表return lists[left];// 拆分区间:计算中间索引,避免(left+right)溢出int mid = left + (right - left) / 2;ListNode* l1 = Msort(lists, left, mid);  // 递归合并左半区间ListNode* l2 = Msort(lists, mid + 1, right);  // 递归合并右半区间// 合并两个子区间的结果,返回合并后的链表return MakeList(l1, l2);}// 辅助函数:合并两个升序链表,返回合并后的升序链表头节点ListNode* MakeList(ListNode* l1, ListNode* l2) {ListNode* head = new ListNode();  // 临时哨兵节点ListNode* tmp = head;  // 游标指针:用于拼接节点// 双指针遍历两个链表,选较小节点接入while (l1 && l2) {if (l1->val <= l2->val) {tmp->next = l1;l1 = l1->next;  // l1指针后移} else {tmp->next = l2;l2 = l2->next;  // l2指针后移}tmp = tmp->next;  // 游标后移}// 接入剩余节点(其中一个链表已遍历完毕)if (l1 != nullptr)tmp->next = l1;if (l2 != nullptr)tmp->next = l2;// 释放哨兵节点,避免内存泄漏ListNode* result = head->next;delete head;return result;}
};

四、总结

三种解法对比(表格)

解法时间复杂度空间复杂度核心优势适用场景
暴力合并O(nk²)O(1)(不含结果)逻辑简单,代码量少面试快速思路验证、k极小场景
优先级队列O(nk log k)O(k)(堆空间)非递归实现,稳定高效面试高频考察、不希望用递归
分治归并O(nk log k)O(log k)(递归栈)空间更优,无额外容器开销追求极致空间效率、熟悉归并思想

关键考点与避坑点

  • 边界处理:必须判断链表为空(lists为空单个链表为空),否则会出现空指针报错;
  • 优先级队列:C++中需自定义比较器实现小顶堆,避免节点值相同时的排序冲突;
  • 分治递归:递归终止条件(l==rl>r)需准确,否则会导致栈溢出或漏合并;
  • 内存管理:哨兵节点使用后需释放,避免内存泄漏(面试中体现代码严谨性)。

核心思想迁移

  • 优先级队列:可迁移至“多数据源有序合并”(如日志合并、数据流Top K);
  • 分治归并:可迁移至“大规模数据排序”(如分布式排序、文件分片合并);
  • 两链表合并:作为基础操作,是后续复杂链表问题的核心组件(如链表拆分、重组)。

五、下题预告

25. K 个一组翻转链表
这道题是链表操作的进阶难题,也是面试高频考点,核心考察“链表局部反转 + 边界衔接”能力,与“合并K个升序链表”形成互补:

  • 前者侧重“链表修改(反转)”,后者侧重“链表合并(有序拼接)”;
  • 需掌握“局部反转模板”“前后区间衔接”“剩余节点处理”三大关键技巧;
  • 能有效区分“会写链表代码”和“能灵活操作链表”的水平,常作为中高级面试的压轴题。

Doro 带着小花🌸来啦!🌸奖励🌸看到这里的你!恭喜你呀,坚持把“合并K个升序链表”的完整解析看到这里,这份耐心和钻研精神就值得大大的肯定!

相信现在的你,不仅掌握了暴力、优先级队列、分治归并三种核心解法,更理清了从“暴力优化到高效算法”的完整思路,下次面试再遇到这类问题,一定能从容不迫地讲清解法、分析复杂度~ 把这篇内容收藏起来,后续刷题时遇到多链表合并、优先级队列应用的题目,随时翻查就能快速唤醒记忆。
关注博主,下一篇我们就直击“K个一组翻转链表”的核心难点,把链表操作的硬骨头彻底啃下来!如果你在今天的学习中有任何疑问——比如优先级队列的比较器定义、分治递归的终止条件理解,或者有更优的解题思路,都可以大胆发到评论区,博主看到会第一时间回复。

对啦,博主最近在找免费无水印的mp4转gif工具,每次做算法图示都被水印困扰,如果屏幕前的你有好用的网站或软件,欢迎在评论区分享,Doro会帮大家把建议妥妥转达给博主,咱们一起帮博主解决这个小难题~ Doro先替博主谢谢大家啦(✧ω✧)
最后别忘了点个赞呀,你的支持就是博主持续更新优质算法内容的最大动力,我们下道题不见不散!
在这里插入图片描述

http://www.dtcms.com/a/597092.html

相关文章:

  • jsp网站开发文献网站开发赚钱
  • 矩阵在图像处理中的应用
  • Nginx集群与SpringCloud Gateway集成Nacos的配置指南
  • 天机学堂-自定义部署详细流程(部署篇:安装虚拟机、docker)
  • 35.微调BERT
  • 【Docker】定义和运行多容器应用程序
  • 蓝桥java数组切割
  • 高级编程培训 | 提升编程能力,助力职业发展的全方位学习路径
  • 【大模型训练】RL中权重更新 学习 reduce_tensor
  • 做网站优化有什么途径公司的企业邮箱怎么查询
  • ComfyUI+RX5700XT+Ubuntu25.04运行配置
  • 【Windows Docker】docker挂载解决IO速度慢的问题
  • 小练11.11
  • 怎么让网站无法自适应可信网站查询
  • 《国内可训练的主流大模型及相关平台》
  • MCP-stdio通信
  • 电商专业培训网站建设网页设计工作岗位及薪资
  • 全球文献智能引擎,突破知网局限
  • 邮件接码API实战教程与代码解析
  • 项目分享|告别枯燥命令行,构建终端用户界面的 TypeScript 库
  • [PowerShell 入门教程]第2天课后作业答案
  • Django中QuerySet 的惰性加载
  • 会议平台网站建设汇通网做期货的网站做期货的网站
  • 【计网】基于三层交换机和 RIP 协议的局域网组建
  • 【系统架构设计】用例技术:需求分析的实用工具
  • 网站设计需要什么软件python基础教程ppt
  • ffmpeg7.1.2-官方示例demo预览
  • 自己怎么优化我网站关键词潍坊尚呈网站建设公司
  • 数据科学每日总结--Day16--数据库
  • 从“高门槛”到“零门槛”:ArcGIS 和 GISBox如何破解中小用户GIS工具使用难题?