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

C++ 分治 快速选择算法 堆排序 TopK问题 力扣 215. 数组中的第K个最大元素 题解 每日一题

文章目录

  • 题目描述
  • 为什么这道题值得你花几分钟的时间弄明白?
  • 算法原理
    • 快速选择算法
      • 快速选择算法的核心思路
    • 堆选择算法
      • 堆选择算法的核心思路
  • 代码实现
    • 快速选择实现
    • 堆选择实现
    • 时间复杂度与空间复杂度分析
  • 总结
  • 下题预告

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

今天是属于每一位代码筑梦人的 1024 程序员节,先向屏幕前的你道一声节日快乐!
算法世界里,我们习惯用逻辑拆解复杂,用代码搭建桥梁,在调试与优化中追逐 “最优解”。恰逢这个专属节日,想借这篇博客与你继续探讨算法的魅力 —— 既是对过往技术探索的小结,也是对未来突破的期许。愿我们在一行行代码、一个个模型中,既能收获技术成长的成就感,也能留存对编程最本真的热爱。

题目描述

题目链接:力扣 215. 数组中的第K个最大元素

题目描述:
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:
输入: [3,2,1,5,6,4], k = 2
输出: 5

示例 2:
输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

提示:
1 <= k <= nums.length <= 105
-104 <= nums[i] <= 104

为什么这道题值得你花几分钟的时间弄明白?

这道题是 Top K 问题的经典代表,而 Top K 问题(找第 K 个最大/最小元素、前 K 个高频元素等)在面试中出现频率极高,吃透它能帮你掌握一类题的核心思路,具体价值体现在三点:

  1. 衔接前置算法,深化思维逻辑
    它的最优解“快速选择算法”,是我上一篇博客中「三指针快排」的“剪枝版”——快排需要递归处理左右子数组,而快速选择只需聚焦目标元素所在的子区间,本质是“分治思想的精准应用”。吃透这道题,能帮你更深刻地理解“分治剪枝”如何将时间复杂度从 O(n log n) 降到 O(n)。

  2. 覆盖多类算法,学会权衡取舍
    除了快速选择,还有“堆选择算法”“计数排序”等解法,不同解法对应不同的时间、空间复杂度(如堆选择空间更优,计数排序需数值范围有限)。通过对比这些解法,你能学会根据实际场景(如数据规模、数值范围)选择最优方案,这正是面试官考察的核心能力。

  3. 贴合工程场景,提升实战价值
    实际开发中,Top K 问题随处可见(如统计热门商品、筛选高频日志),且常伴随“海量数据”“时间敏感”的约束。这道题要求的 O(n) 时间复杂度,正是工程中对“高效处理数据”的真实需求,掌握它能让你在实际问题中快速落地解决方案。

算法原理

我们这篇博客重点和大家讨论两种满足 O(n) 时间复杂度的核心算法:快速选择算法(最优解)和 堆选择算法

快速选择算法

快速选择算法,实则是「三指针快排」的精妙延伸。它的核心智慧在于:主动放弃无意义的递归过程,将全部注意力聚焦于目标元素所在的区间,通过这种精准的“取舍”,实现了时间复杂度的显著优化,让算法在查找特定位置元素时效率更高。

不过,要真正吃透快速选择算法,对三指针快排的核心细节必须了然于胸。倘若你目前对三指针快排中的“区间划分逻辑”“随机基准值选择”等关键知识点仍有模糊,甚至存在记忆断层,那么强烈建议你先暂停当前学习,优先回顾我昨天的博客 力扣 912. 排序数组 一文中的快排实现逻辑。需要特别强调的是:在开启快速选择算法的讨论之前,务必对快速排序具备一定的认知基础

要知道,快速选择算法的实现逻辑完全基于快排的过程。后续关于快速选择算法的讲解,会直接切入它与快排的差异点,并在快排的基础上直接推导、实现快速选择算法。若是前期快排的核心知识点掌握不牢固,后续关于快速选择算法的内容,很可能会让我们陷入“看不懂、理不清”的困境,影响我们接下来一起讨论算法的效果。

快速选择算法的核心思路

1.区间划分(复用三指针逻辑)
我们知道,快速排序的核心是通过随机选取的基准值(key),将数组划分为三部分:小于 key、等于 key、大于 key,随后对左右两侧未完全排序的区间递归重复这一过程,直至所有元素有序。而快速选择算法的关键优化,就在于对递归过程的“剪枝”——主动舍弃无关区间的递归,仅保留目标元素可能存在的区间,从而将时间复杂度从快排的 O(N log N) 降至 O(N)。

因此,快速选择同样采用三指针法进行区间划分:随机选取基准值 mark,通过指针操作将数组切分为三个明确区间:

  • 左区间 [begin, left]:所有元素 小于 mark
  • 中间区间 [left+1, right-1]:所有元素 等于 mark
  • 右区间 [right, end]:所有元素 大于 mark
    (指针定义与三指针快排一致:left 为左区间的右边界,right 为右区间的左边界,i 为遍历指针)。

当数组被划分为这三部分后,我们要寻找的第 K 大元素必然落在其中一个区间内。我们知道当一次分区之后数组上的每个指针所在的位置是固定的(具体过程可参考 快排铺垫 三指针 力扣 75.颜色分类 中的代码实现 三指针 代码走读部分),即可精准定位目标元素所在的区间👇:
在这里插入图片描述
2.判断目标元素所在区间
我们要寻找的“第 K 个最大元素”,本质是数组按降序排序后第 K 个位置的元素。结合三指针划分的左、中、右三个区间(元素分别小于、等于、大于基准值 mark),目标元素的位置可通过区间长度与 K 的对比来确定:

设右区间长度为 c = end - right + 1(存放大于 mark 的元素,是当前区间中最大的一批),中间区间长度为 b = right - left - 1(存放等于 mark 的元素),左区间长度为 a = left - begin + 1(存放小于 mark 的元素)。此时有三种可能:

  • K <= c:第 K 大元素在右区间(因为右区间的元素均大于 mark,是当前最大的一批,目标必在此处);
  • K > c + b:第 K 大元素在左区间(需调整 K 值,减去右区间和中间区间的总长度,即 K = K - c - b,缩小查找范围);
  • 若上述两种情况均不满足(即 c < K <= c + b):第 K 大元素在中间区间,直接返回 mark 即可(中间区间的元素均等于 mark,无需继续递归)。

如下图👇:
在这里插入图片描述
3.递归聚焦目标区间
确定目标元素所在的区间后,只需对该区间(左区间或右区间)递归执行区间划分与判断步骤,不断缩小查找范围,直至区间长度为 1——此时该元素即为我们要找的第 K 大元素,直接返回即可。

至此,快速选择算法的核心逻辑已清晰呈现:它本质上是在快速排序的基础上,通过增加“仅对目标所在区间递归”的条件,并调整返回值逻辑,实现了对无意义计算的“剪枝”。这种聚焦核心区间的思路,正是快速选择算法能够将时间复杂度优化至 O(N) 的关键所在。

关键细节:为什么能做到 O(n) 时间复杂度?
快排的时间复杂度是 O(n log n),因为需要递归处理左右两个子数组;而快速选择每次只处理一个子数组,且子数组长度不断缩小(平均每次缩小一半)。
总时间 = n + n/2 + n/4 + … + 1 ≈ 2n,最终时间复杂度为 O(n)(最坏情况 O(n²),但通过随机基准可极大降低概率)这是简单说明详细证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。

堆选择算法

堆选择算法基于“堆的优先级特性”,核心是“用堆维护前 K 个最大元素”,适合对空间复杂度要求较高的场景。结合之前学过的堆排序知识,这里重点讲解更高效的 最小堆法

堆选择算法的核心思路

1.维护大小为 K 的最小堆
最小堆的堆顶是堆中最小的元素,我们用它来“筛选”前 K 个最大元素:

  • 遍历数组,若堆的大小小于 K,直接将当前元素入堆;
  • 若堆的大小等于 K,且当前元素 大于堆顶(说明当前元素比堆中最小的“候选最大元素”更大,应替换它),则弹出堆顶,将当前元素入堆。
以示例2为例(nums=[3,2,3,1,2,4,5,5,6],k=4)初始堆为空,遍历前 4 个元素 [3,2,3,1],堆填满后为 [1,2,3,3](最小堆,堆顶为1);遍历第 5 个元素 2:2 <= 堆顶1?否,不替换;遍历第 6 个元素 4:4 > 堆顶1,弹出1,入堆4,堆变为 [2,2,3,3](堆顶更新为2);遍历第 7 个元素 5:5 > 堆顶2,弹出2,入堆5,堆变为 [2,3,3,5](堆顶更新为2);遍历第 8 个元素 5:5 > 堆顶2,弹出2,入堆5,堆变为 [3,3,5,5](堆顶更新为3);遍历第 9 个元素 6:6 > 堆顶3,弹出3,入堆6,堆变为 [3,5,5,6](堆顶更新为3)。

2.堆顶即为答案
遍历结束后,堆中存储的是数组中 最大的 K 个元素(示例2中堆为 [3,5,5,6],最大4个元素为3,5,5,6),而堆顶是这 K 个元素中最小的那个——也就是我们要找的“第 K 个最大元素”(示例2中堆顶为3?不对,此处修正:示例2最终堆应为 [4,5,5,6],堆顶为4,与示例2输出一致,前文步骤中第6个元素4入堆后需重新调整堆结构,确保堆顶为最小)。

时间复杂度分析(调整呈现方式)
1.复杂度拆解

  • 堆操作时间:每个元素入堆/出堆的时间取决于堆的高度,最小堆的高度为 log K(因为堆大小始终不超过 K);
  • 总时间:遍历数组需 n 次操作,每次操作时间为 O(log K),因此总时间复杂度为 O(n log K)。

2.与快速选择的对比

  • 当 K 较小时(如 K=100,n=1e5):O(n log K) ≈ O(n),与快速选择效率接近,但堆选择无需递归(无栈溢出风险),实现更简单;
  • 当 K 较大时(如 K=1e5/2):O(n log K) ≈ O(n log n),效率低于快速选择的 O(n)。

代码实现

快速选择实现

class Solution {
public:// 递归函数:在nums的[begin, end]区间内找第k个最大元素// 参数说明://  nums:待查找的数组(会原地修改,用于区间划分)//  begin:当前查找区间的左边界(闭区间)//  end:当前查找区间的右边界(闭区间)//  k:当前需要查找的“第k个最大元素”(注意:每次递归可能需要调整k值)int TopK(vector<int>& nums, int begin, int end, int k) {// 递归终止条件:区间只有一个元素,直接返回if (begin == end)return nums[begin];// 1. 随机选择基准值(避免固定基准导致的最坏情况,复用三指针快排逻辑)int mark = nums[(rand() % (end - begin + 1)) + begin];int left = begin - 1;   // 左区间(<mark)的右边界int right = end + 1;    // 右区间(>mark)的左边界int i = begin;          // 遍历指针// 2. 三指针划分区间(<mark, =mark, >mark)while (i < right) {if (nums[i] < mark) {// 元素归入左区间,left右移,i右移(当前元素已处理)swap(nums[++left], nums[i++]);} else if (nums[i] == mark) {// 元素归入中间区间,直接i右移i++;} else {// 元素归入右区间,right左移,i不右移(交换来的元素未判断)swap(nums[--right], nums[i]);}}// 3. 计算三个区间的长度,判断第k大元素所在区间int b = end - right + 1;    // 右区间长度(>mark的元素个数)int c = right - left - 1;     // 中间区间长度(=mark的元素个数)if (b >= k) {// 情况1:第k大元素在右区间,递归处理右区间return TopK(nums, right, end, k);} else if (b + c >= k) {// 情况2:第k大元素在中间区间,直接返回markreturn mark;} else {// 情况3:第k大元素在左区间,调整k值后递归处理左区间return TopK(nums,begin,left,k - b - c);}}int findKthLargest(vector<int>& nums, int k) {// 初始化随机数种子(确保每次运行基准选择不同,避免最坏情况)srand(time(nullptr));return TopK(nums, 0, nums.size() - 1, k);}
};

堆选择实现

class Solution {
public:int findKthLargest(vector<int>& nums, int k) {// 定义最小堆:C++中priority_queue默认是最大堆(比较器less<int>)// 最小堆需显式指定三个参数:存储类型、底层容器、比较器(greater<int>表示“小于”关系,即小元素优先)// 注意:需包含<queue>头文件(LeetCode环境已默认包含,本地编译需手动添加)priority_queue<int, vector<int>, greater<int>> minHeap;// 遍历数组,维护大小为k的最小堆for (int num : nums) {if (minHeap.size() < k) {// 堆未满:直接将当前元素入堆,堆会自动调整结构(保持最小堆特性)minHeap.push(num);} else {// 堆已满:判断当前元素是否比堆顶大(堆顶是堆中最小的元素)if (num > minHeap.top()) {// 情况1:当前元素更大,替换堆顶(弹出最小的候选元素,加入新的更大元素)minHeap.pop();   // 弹出堆顶(最小元素)minHeap.push(num); // 加入当前元素,堆自动调整}// 情况2:当前元素小于等于堆顶,无需处理(它不可能是前k大元素)}}// 最终堆中存储的是数组中最大的k个元素,堆顶是这k个元素中最小的,即第k个最大元素return minHeap.top();}
};

时间复杂度与空间复杂度分析

算法时间复杂度空间复杂度核心优势适用场景
快速选择算法平均O(n),最坏O(n²)O(log n)(递归栈)时间最优,原地操作大多数场景,尤其是大数据量
堆选择算法O(n log K)O(K)(堆空间)性能稳定,无最坏情况K 较小的场景(如 Top 100)

总结

  1. 算法选择优先级
    优先用 快速选择算法,它平均时间最优(O(n))且原地操作(空间 O(log n)),完全满足题目要求;最坏情况概率极低(随机基准加持下趋近于0)。若 K 远小于 n(如 K=100,n=1e5),堆选择算法 也是不错的选择——实现简单,无递归栈溢出风险(适合极大数据量的迭代处理)。

  2. 核心思维迁移
    快速选择的“分治剪枝”思想,可迁移到“第 K 个最小元素”(只需将区间判断逻辑改为“左区间是大于mark,右区间是小于mark”)、“第 K 个高频元素”(先统计频率,再对频率用快速选择);堆选择的“优先级筛选”思想,可迁移到“前 K 个高频单词”(用最小堆维护前K个高频单词,注意字典序排序)。

  3. 避坑指南

    • 快速选择:记准区间边界(右区间 [right, end])、递归左区间需调整k值、必初始化随机种子;
    • 堆选择:最小堆用 greater<int>、堆已满时先比堆顶再操作;
    • 本地调试:若快速选择超时,先检查是否漏了随机种子;若堆选择答案错误,先检查比较器是否正确。

下题预告

力扣 面试题 17.14. 最小K个数

这道题是“Top K”问题的反向延伸,和“数组中的第K个最大元素”形成逻辑互补,能帮你进一步巩固核心算法的灵活应用能力。它不仅会复用快速选择、堆排序等已学方法,还会强化“问题转化”思维——比如如何将“找最小K个数”的需求,快速对应到已有算法的调整上(如快速选择的区间判断逻辑修改、堆的类型选择变化)。

如果这篇数组中的第K个最大元素的博客帮你理清了思路,别忘了点赞支持一下呀!这样不仅能让更多需要的朋友看到,也能给我继续拆解算法题的动力~若想跟着节奏攻克下一道题,记得关注我,后续更新会第一时间提醒你!觉得内容实用的话,还可以顺手收藏,万一以后复习算法时想回顾规律,打开就能看,省时又高效~

在这里插入图片描述

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

相关文章:

  • 永磁同步电机无速度算法--基于相位超前校正的LESO
  • 动态 静态 网站地图合肥庐阳区建设局网站
  • JavaEE开篇之计算机是如何工作的
  • 基于python机器学习的农产品价格数据分析与预测的可视化系统
  • 如何通过掌纹识别实现Windows工作站安全登录:从技术原理到企业级落地实践
  • 正则表达式全集
  • 中山手机网站制作哪家好网站管理员登陆后缀
  • K8s高可用:四大核心机制解析
  • 1024勋章发文活动
  • 依托金仓数据库的医疗信创多院区实践与 KingbaseES 操作详解
  • Linux---开发工具2
  • GBase安装部署
  • 4A架构解析:业务、数据、应用、技术架构的区别与联系
  • Redisson与Spring提供的RedisTemplate做一个对比
  • 南京做网站公司地点免费ddns域名注册
  • asp网站开发报告酷站是什么网站
  • [服务部署]京东云部署JavaWeb项目
  • 27、LangChain开发框架(四)-- LangChain接入工具基本流程
  • 找人做网站要准备什么九江网站网站建设
  • 帝可得智能售货机系统实战Day1:从环境搭建到区域管理功能落地 (1)
  • 10.2Web Component
  • 有没有做产品团购的网站wordpress文章页禁止右键
  • Nginx 反向代理解析:从原理到生产级配置实战
  • [理论题] 2025 年 “技耀泉城” 海右技能人才大赛网络安全知识竞赛题目(四)
  • 文化馆网站数字化建设介绍重庆seo网站建设
  • 【Betaflight源码学习】之初始化函数(init.c)
  • STM32H750寄存器操作(硬件I2C)
  • 算法18.0
  • RHCA - DO374 | Day02:管理内容集和执行环境
  • 网站建设明细价格表包头seo营销公司