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

【力扣100题】91.数组中的第K个最大元素

一、题目描述

给定整数数组nums和整数k,返回数组中第k个最大的元素。

注意:找的是数组排序后的第k个最大的元素,而不是第k个不同的元素。

示例 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 <= 10^5
  • -10^4 <= nums[i] <= 10^4

二、解题思路总览

方法时间复杂度空间复杂度说明
排序后直接取值O(n log n)O(1)排序后直接取第 n-k 个元素
堆排序(大顶堆)O(n log n)O(1)建堆 + 逐个弹出直到第 k 个
小顶堆(优化)O(n log k)O(k)维护大小为 k 的堆,适合大数据流
快速选择O(n)O(1)基于 partition 的选择算法
计数排序O(n + R)R=数值范围,适合元素值范围小的场景

三、方法一:快速选择(推荐)

3.1 核心思想

快速选择是基于快速排序的选择算法,每次 partition 后只递归一边,所以时间复杂度是 O(n)。

3.2 算法流程图

原始数组: [3, 2, 1, 5, 6, 4], k = 2 目标: 找第 2 大的元素,即 target_index = 6 - 2 = 4 初始状态: +---+---+---+---+---+---+ | 3 | 2 | 1 | 5 | 6 | 4 | +---+---+---+---+---+---+ L R +------------------------------------------------------------+ | Step 1: partition(0, 5) | | | | 随机选择 pivot = nums[3] = 5 | | 交换到 left: [5, 2, 1, 3, 6, 4] | | | | 双指针扫描: | | i -> <- j | | [5, 2, 1, 3, 6, 4] | | | | 交换 2 和 6: [5, 2, 1, 3, 6, 4] -> i++,j-- -> i=3,j=2 | | i >= j,退出循环 | | 交换 nums[left] 和 nums[j]: [3, 2, 1, 5, 6, 4] | | 返回 pivot_index = 3 | +------------------------------------------------------------+ 比较: pivot_index(3) vs target_index(4) pivot_index < target_index -> 搜索右半部分 [4, 5] +------------------------------------------------------------+ | Step 2: partition(4, 5) | | | | 随机选择 pivot = nums[5] = 4 | | 交换到 left: [3, 2, 1, 5, 4, 6] | | | | 双指针扫描: | | i -> <- j | | [3, 2, 1, 5, 4, 6] | | | | i >= j,退出循环 | | 交换 nums[left] 和 nums[j]: [3, 2, 1, 5, 4, 6] | | 返回 pivot_index = 4 | +------------------------------------------------------------+ 比较: pivot_index(4) == target_index(4) -> 找到答案! 结果: nums[4] = 4

3.3 完整代码

classSolution{public:intpartition(vector<int>&nums,intleft,intright){// 随机选择 pivot,避免最坏情况inti=left+rand()%(right-left+1);intpivot=nums[i];swap(nums[i],nums[left]);i=left+1;intj=right;while(1){while(i<=j&&nums[i]<pivot){i++;}while(i<=j&&nums[j]>pivot){j--;}if(i>=j)break;swap(nums[i],nums[j]);i++;j--;}swap(nums[left],nums[j]);returnj;}intfindKthLargest(vector<int>&nums,intk){srand(time(NULL));intn=nums.size();inttarget_index=n-k;intleft=0,right=n-1;while(true){inti=partition(nums,left,right);if(i==target_index){returnnums[i];}if(i>target_index){right=i-1;}else{left=i+1;}}}};

3.4 复杂度分析

情况时间复杂度说明
平均O(n)每次 partition 大致将问题规模减半
最坏O(n^2)pivot 选择不当(有序数组选最小值)

空间复杂度:O(1)

时间复杂度推导:

T(n) = n + n/2 + n/4 + n/8 + ... = n * (1 + 1/2 + 1/4 + ...) = n * 2 = O(n)

四、方法二:小顶堆(适合大数据流)

4.1 核心思想

维护一个大小为 k 的小顶堆,堆顶是当前第 k 大的元素。遍历数组时,比堆顶大的元素才入堆,最终堆顶就是答案。

4.2 算法流程图

数组: [3, 2, 1, 5, 6, 4], k = 2 +------------------------------------------------------------+ | 初始化小顶堆(大小限制为 k=2) | +------------------------------------------------------------+ 遍历元素: +------------------------------------------------------------+ | 插入 3: 堆=[3] | | 3 | | | | 插入 2: 堆=[2,3] | | 2 | | / | | 3 | +------------------------------------------------------------+ +------------------------------------------------------------+ | 插入 1: 堆=[1,3,2] -> 调整 -> 堆=[1,3,2](大小超过2,删除最小)| | 1 | | / \ | | 3 2 | | | | 删除堆顶 1: 堆=[2,3] | | 2 | | / | | 3 | +------------------------------------------------------------+ +------------------------------------------------------------+ | 插入 5: 5 > 堆顶(2),入堆 | | 堆=[2,3,5] -> 调整 -> 堆=[2,5,3](大小超过2,删除最小) | | 2 | | / \ | | 3 5 | | | | 删除堆顶 2: 堆=[3,5] | | 3 | | / | | 5 | +------------------------------------------------------------+ +------------------------------------------------------------+ | 插入 6: 6 > 堆顶(3),入堆 | | 堆=[3,5,6] -> 调整 -> 堆=[3,5,6](大小超过2,删除最小) | | 3 | | / \ | | 5 6 | | | | 删除堆顶 3: 堆=[5,6] | | 5 | | / | | 6 | +------------------------------------------------------------+ +------------------------------------------------------------+ | 插入 4: 4 > 堆顶(5),入堆 | | 堆=[4,6,5] -> 调整 -> 堆=[4,5,6](大小超过2,删除最小) | | 4 | | / \ | | 5 6 | | | | 删除堆顶 4: 堆=[5,6] | +------------------------------------------------------------+ 最终堆顶 = 5,即第 2 大的元素

4.3 完整代码

classSolution{public:intfindKthLargest(vector<int>&nums,intk){priority_queue<int,vector<int>,greater<int>>minHeap;for(intnum:nums){minHeap.push(num);if(minHeap.size()>k){minHeap.pop();}}returnminHeap.top();}};

4.4 复杂度分析

复杂度说明
时间O(n log k)n 个元素,每个入堆 O(log k)
空间O(k)堆的大小

五、方法三:大顶堆 + 弹出

5.1 核心思想

将整个数组建成大顶堆,然后弹出 k 次堆顶,最后一次弹出的就是第 k 大的元素。

5.2 算法流程图

数组: [3, 2, 1, 5, 6, 4], k = 2 +------------------------------------------------------------+ | Step 1: 建大顶堆(从最后一个非叶子节点开始下沉) | +------------------------------------------------------------+ 初始数组完全二叉树: 3 / \ 2 1 / \ / 5 6 4 下沉 i=2 (节点2): 3 / \ 6 1 / \ / 5 2 4 下沉 i=1 (节点3): 6 / \ 5 4 / \ / 3 2 1 下沉 i=0 (节点6): 6 / \ 5 4 / \ / 3 2 1 +------------------------------------------------------------+ | Step 2: 弹出堆顶 k=2 次 | +------------------------------------------------------------+ 第1次弹出: 堆顶 6 与 末尾 1 交换 -> [1,5,4,3,2,6] 堆大小-1,堆顶下沉 -> [5,3,4,1,2,6] 堆=[5,3,4,1,2] 第2次弹出: 堆顶 5 与 末尾 2 交换 -> [2,3,4,1,5,6] 堆大小-1,堆顶下沉 -> [4,3,2,1,5,6] 堆=[4,3,2,1] 答案 = 4(弹出的第2个元素)

5.3 完整代码

classSolution{public:intfindKthLargest(vector<int>&nums,intk){intn=nums.size();// 建大顶堆for(inti=n/2-1;i>=0;i--){heapify(nums,n,i);}// 弹出 k 次for(inti=0;i<k;i++){swap(nums[0],nums[n-1-i]);heapify(nums,n-1-i,0);}returnnums[n-k];}private:voidheapify(vector<int>&nums,intsize,inti){intlargest=i;intleft=2*i+1;intright=2*i+2;if(left<size&&nums[left]>nums[largest]){largest=left;}if(right<size&&nums[right]>nums[largest]){largest=right;}if(largest!=i){swap(nums[i],nums[largest]);heapify(nums,size,largest);}}};

5.4 复杂度分析

复杂度说明
时间O(n log n)建堆 O(n) + 弹出 k 次 O(k log n)
空间O(1)原地调整

六、方法四:排序后直接取值

6.1 核心思想

对数组升序排序,然后直接取第 n-k 个元素。

6.2 完整代码

classSolution{public:intfindKthLargest(vector<int>&nums,intk){sort(nums.begin(),nums.end());returnnums[nums.size()-k];}};

6.3 复杂度分析

复杂度说明
时间O(n log n)排序的时间复杂度
空间O(1)若使用原地排序

七、方法五:计数排序(适用于值范围小的场景)

7.1 核心思想

因为nums[i]范围是-10^410^4,共 20001 个可能的值。统计每个值出现的次数,然后从最大值开始累加,找到第 k 大的元素。

7.2 算法流程图

数组: [3, 2, 1, 5, 6, 4], k = 2 元素范围: -10^4 ~ 10^4 -> 需要统计 20001 个可能的值 +------------------------------------------------------------+ | 统计每个值出现的次数 | +------------------------------------------------------------+ 遍历数组: 3 -> count[3+offset]++ 2 -> count[2+offset]++ 1 -> count[1+offset]++ 5 -> count[5+offset]++ 6 -> count[6+offset]++ 4 -> count[4+offset]++ 计数数组(部分): ... 1:1, 2:1, 3:1, 4:1, 5:1, 6:1 ... +------------------------------------------------------------+ | 从最大值开始累加,找到第 k 大的元素 | +------------------------------------------------------------+ 从 6 开始向左累加: count[6] = 1 -> 累加和 = 1 < k(2),继续 count[5] = 1 -> 累加和 = 2 == k(2) -> 答案 = 5 答案 = 5

7.3 完整代码

classSolution{public:intfindKthLargest(vector<int>&nums,intk){constintOFFSET=10000;// 偏移量,将负数转为正数索引constintRANGE=20001;// -10000 到 10000,共 20001 个值vector<int>count(RANGE,0);// 统计每个值出现的次数for(intnum:nums){count[num+OFFSET]++;}// 从最大值开始累加for(inti=RANGE-1;i>=0;i--){k-=count[i];if(k<=0){returni-OFFSET;}}return0;// 不会走到这里}};

7.4 复杂度分析

复杂度说明
时间O(n + R)n=数组长度,R=数值范围(20001)
空间计数数组大小

八、方法对比总结

方法时间复杂度空间复杂度适用场景
快速选择O(n) 平均O(1)通用场景,要求 O(n) 时首选
小顶堆O(n log k)O(k)大数据流,TOP-K 问题
大顶堆O(n log n)O(1)需要完整排序时
排序后取值O(n log n)O(1)简单直接,代码简洁
计数排序O(n + R)值范围小(如本题 -104~104)
时间复杂度对比(假设 n=100000, k=100): 快速选择: O(n) = 100000 ★最优 小顶堆: O(n log k) = 100000 * 7 ≈ 700000 大顶堆: O(n log n) = 100000 * 17 ≈ 1700000 排序: O(n log n) = 100000 * 17 ≈ 1700000 计数排序: O(n + R) = 100000 + 20001 ≈ 120000 ★(本题最优) 空间复杂度对比: 快速选择: O(1) ★最优 小顶堆: O(k) ★ 大顶堆: O(1) ★ 排序: O(1) ★ 计数排序: O(R) (R=20001)

九、面试追问 FAQ

问题回答
Q: 快速选择为什么是 O(n) 而不是 O(n log n)?因为每次 partition 只递归一边,T(n) = n + n/2 + n/4 + … = 2n = O(n)
Q: 快速选择最坏情况是什么?有序数组 + 每次选最小/最大 pivot,退化为 O(n^2)。解决方法是随机选择 pivot
Q: 小顶堆和大顶堆的选择?TOP-K 问题用小顶堆(维护 k 个最大),全部排序用大顶堆
Q: 为什么快速选择空间是 O(1)?原地 partition,只用几个指针变量。递归栈深度 O(log n),不算额外空间
Q: 计数排序的限制?只能用于值范围有限的整数排序,且范围不能太大
Q: 稳定性的考虑?本题不要求稳定性。堆排序不稳定,快速选择也不稳定

十、相关题目

题目难度关键点
215. 数组中的第K个最大元素中等快速选择 / 堆排序
347. 前 K 个高频元素中等桶排序 / 堆排序
703. 数据流中的第 K 大元素简单小顶堆
973. 最接近原点的 K 个点中等堆排序 / 快速选择

十一、总结

要点说明
核心思想求第 K 大 -> 转换为求下标 n-k 的元素
快速选择平均 O(n),随机 pivot 避免退化
小顶堆O(n log k),适合大数据流 TOP-K
选择依据根据数据规模和值范围选择合适方法

http://www.jsqmd.com/news/1012497/

相关文章:

  • Android 16进程永生架构:突破性保活技术实现无权限自启动与防卸载机制
  • AI编排:企业级LLM落地的数据调度与系统集成方法论
  • Spring Boot 的核心注解 @SpringBootApplication 由哪三个注解组成?
  • BongoCat终极指南:让你的桌面猫咪活起来的完整教程
  • MPC8313E IPIC中断控制器:从原理到实战配置与优化
  • Arduino红外遥控终极指南:从零开始掌握红外信号收发技术
  • 10分钟掌握hCaptcha Challenger:用AI轻松破解验证码的终极指南
  • ViT模型效果真比CNN强?我用CIFAR-10和ImageNet数据集实测给你看
  • 2026年安徽合肥女孩中考没考上高中上什么学校好? - 我叫小周
  • 湖州装修公司怎么选?2026年湖州靠谱装修公司推荐攻略 - 匠言榜单
  • 网页突然消失?这个浏览器扩展让你再也不怕404错误
  • Paperless-ngx多语言配置指南:打造全球化文档管理系统
  • (6月最新)深挖嘉兴GEO行业,十家高口碑优化公司资质效果大盘点 - 玖叁鹿
  • 如何用Wayback Machine浏览器扩展永久保存互联网记忆:终极网页存档指南
  • 微服务异步场景链路断裂完整解决方案
  • 别再只看价格了!阿里云、AWS、GCP隐藏成本大起底(附账单优化技巧)
  • 2026年六安家长必看:孩子落榜别将就,共达复读班再战一年稳上全日制大专联系方式多少?官方最新发布 - cc江江
  • SpringBoot项目实战:构建高可用的电商系统
  • 华硕笔记本轻量化控制革命:G-Helper如何替代Armoury Crate提升用户体验
  • 微信好友关系检测工具技术架构深度解析:从模拟协议到Hook技术的演进路径
  • 26年6月湖州企业引流首选!十大靠谱GEO优化服务商全方位测评 - 玖叁鹿
  • Notepad--:三分钟上手国产跨平台文本编辑利器
  • 宇舶腕表官方售后服务体系全解析(2026年6月最新版) - 亨得利官方服务中心
  • Agent 的刹车:一文讲透 HITL(Human-in-the-Loop)
  • 淮南职业技术学院中专部招生办电话多少?报名有哪些要求?2026年官方解答 - hflgzz
  • 2026 企业级大模型服务商深度解析:百度、阿里、字节、月之暗面能力横评
  • LSTM时间序列预测实战:疫情数据建模与工程落地
  • 从管理百人团队到单兵研发:初创 CEO 必须跨越的工具提效与代码自律门槛
  • 2026 高端奢侈品回收报价排行,南京五大箱包回收门店实测 TOP5 - 讯息早知道
  • Steam挂刀行情站:24小时监控四大平台饰品价格的完整指南