快速选择 vs 最小堆:如何在数组中高效找到第K大元素?

news2024/10/5 3:28:30

问题分析

在这个问题中,任务是找到数组中的第 k 大元素。虽然可以通过对整个数组排序后直接选择第 k 大元素,但这种方法的时间复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn),在处理大规模数据时不够高效。因此,我们需要设计一种时间复杂度为 O ( n ) O(n) O(n) 的算法来解决这个问题。

215. 数组中的第K个最大元素 - 力扣(LeetCode)

本文将详细介绍两种常见且高效的解决方案:快速选择算法(Quickselect)最小堆(Min-Heap)算法。这两种方法的时间复杂度都能接近 O ( n ) O(n) O(n),尤其适合处理大数据。

方法一:快速选择算法(Quickselect)

快速选择算法是快速排序的变体,能在不完全排序数组的情况下找到第 k k k 大元素。其核心思想是分治法,每次通过选择一个基准元素(pivot)将数组分区(partition),通过一次划分即可确定基准元素的最终位置。基于基准值的位置,可以递归地缩小问题的规模,直至找到目标元素。

思路:

  1. 选择一个基准值(pivot),这里通过三分法(选取中间位置的元素作为基准)来保证分区的平衡性。
  2. 对数组进行划分操作,将所有比基准值大的元素放在基准值的左边,比基准值小的元素放在基准值的右边。
  3. 比较基准值的最终位置与目标位置 k 的关系:
    • 如果基准值的位置正好是第 k k k 大的元素,则直接返回基准值。
    • 如果基准值的位置比 k 大,递归处理左边的子数组。
    • 如果基准值的位置比 k 小,递归处理右边的子数组。

这种方法的优势在于,不需要完全排序整个数组,而是通过划分确定一个元素的最终位置,从而减少问题规模。平均情况下,每次划分能将数组分成两半,时间复杂度为 O ( n ) O(n) O(n)

快速选择算法的实现(C++):

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 快速选择辅助函数
int quick_select(vector<int>& nums, int l, int r, int k) {
    if (l >= r) return nums[k];  // 递归结束条件,当区间内只有一个元素时直接返回
    int i = l - 1, j = r + 1;
    int x = nums[l + (r - l) / 2];  // 选择中间元素作为基准值
    
    while (i < j) {
        do { i++; } while (nums[i] > x);  // 找到第一个小于或等于基准值的元素
        do { j--; } while (nums[j] < x);  // 找到第一个大于或等于基准值的元素
        if (i < j) swap(nums[i], nums[j]);  // 交换两个不符合条件的元素
    }
    
    // 根据基准值位置判断递归方向
    if (k <= j) return quick_select(nums, l, j, k);  // 在左半部分递归查找
    else return quick_select(nums, j + 1, r, k);  // 在右半部分递归查找
}

// 主函数,查找第 k 大的元素
int findKthLargest(vector<int>& nums, int k) {
    return quick_select(nums, 0, nums.size() - 1, k - 1);  // 注意索引从 0 开始
}

示例分析:

  • 输入:nums = [3,2,1,5,6,4]k = 2
  • 目标是找到第2大的元素。
  • 第一次划分时,选择中间元素 4 作为基准值。经过划分操作,数组变为 [5, 6, 4, 1, 2, 3],基准值 4 位于索引2。
  • 基准值 4 的位置比目标位置 k-1=1 大,因此递归处理左边的子数组 [5, 6]
  • 第二次划分时,选择 6 作为基准值,数组变为 [6, 5],基准值位于索引0。
  • 基准值 6 恰好是第1大的元素,因此第2大的元素是 5,最终返回结果 5

时间复杂度分析:

  • 平均时间复杂度 O ( n ) O(n) O(n),在随机选择基准值的情况下,每次划分可以将问题规模缩小一半。
  • 最坏情况时间复杂度 O ( n 2 ) O(n^2) O(n2),当每次划分极不平衡时,例如数组已排好序时,可能会导致每次只缩小一个元素的规模。
  • 空间复杂度 O ( 1 ) O(1) O(1),这是原地排序算法,不需要额外的存储空间。递归调用的栈空间为 O ( log ⁡ n ) O(\log n) O(logn),因递归的深度与数组大小相关。

小结:

这种快速选择算法通过类似快速排序的划分技术,以平均 O ( n ) O(n) O(n) 的时间复杂度有效地查找数组中的第 k 大元素。相比于完全排序整个数组后再提取目标元素,这种方法在大多数情况下更加高效。


方法二:最小堆算法(Min-Heap)

最小堆是一种二叉堆数据结构,它能在常数时间内找到堆顶元素,并且能在对数时间内插入新元素或移除堆顶元素。我们可以使用最小堆来解决这个问题,维护一个大小为 k 的最小堆,从而始终保持前 k 大的元素在堆中。遍历数组后,堆顶的元素就是第 k 大的元素。

在C++中,标准库提供了 priority_queue 容器来实现堆结构。在默认情况下,priority_queue 是最大堆,但是我们可以自定义为最小堆来满足本题需求。

C++ 中的 priority_queue

priority_queue 是一种基于二叉堆的数据结构,可以高效地进行插入和删除堆顶元素操作。在默认情况下,它是一个最大堆,即堆顶元素始终是堆中最大的元素。但是我们可以通过传递自定义的比较器,将其转换为最小堆。在本题中,最小堆用于维护前 k 大元素。

priority_queue 详细解释

1. 定义与用法

priority_queue 是一种优先级队列,按指定优先级顺序处理队列中的元素。其实现基于堆(通常为二叉堆),提供以下常见操作:

  • push():插入一个新元素。
  • pop():移除堆顶元素(对于最大堆来说,堆顶是最大元素;对于最小堆来说,堆顶是最小元素)。
  • top():返回堆顶元素。
  • size():返回优先队列中元素的数量。
  • empty():检查优先队列是否为空。

priority_queue 的默认实现中,它是一个最大堆,即 top() 函数返回的是队列中的最大元素。

2. 最小堆的实现

我们可以通过自定义比较器,将 priority_queue 变为最小堆。在 C++ 中,标准库中提供了 greater<T> 作为比较器,用于实现从小到大的顺序,因此最小堆的声明如下:

priority_queue<int, vector<int>, greater<int>> minHeap;
  • int:堆中存储的元素类型。
  • vector<int>:底层容器,默认使用 vector
  • greater<int>:表示采用从小到大的顺序排列堆中元素,即最小堆。
3. 插入与删除操作的时间复杂度

每次插入或删除堆顶元素的时间复杂度为 O ( log ⁡ k ) O(\log k) O(logk),其中 k 是堆的大小。在查找第 k 大元素的过程中,最小堆的大小始终维持在 k,因此插入和删除操作的代价不会随数组的长度 n 增长而变化。

4. 堆顶元素的维护

在本题中,我们始终维持最小堆的大小为 k,也就是说堆中只存储当前数组中最大的 k 个元素。堆顶始终是这 k 个元素中最小的那个元素,因此当遍历完数组后,堆顶的元素即为第 k 大的元素。

思路:

  1. 使用一个大小为 k 的最小堆。
  2. 遍历数组,将每个元素逐一加入堆中。
    • 如果堆的大小超过 k,就移除堆顶元素(堆顶是最小的元素),从而只保留 k 个最大的元素。
  3. 遍历结束后,堆顶元素即为第 k 大的元素。

这种方法的优势是适用于需要频繁查找第 k 大元素的情况,尤其适合处理数据流或无法一次性加载到内存的大规模数据。其时间复杂度为 O ( n log ⁡ k ) O(n \log k) O(nlogk),因为每次插入操作的复杂度为 O ( log ⁡ k ) O(\log k) O(logk)

最小堆算法的实现(C++):

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

// 主函数
int findKthLargest(vector<int>& nums, int k) {
    // 定义一个最小堆
    priority_queue<int, vector<int>, greater<int>> minHeap;
    
    for (int num : nums) {
        minHeap.push(num);  // 插入元素
        if (minHeap.size() > k) {  // 堆的大小超过 k 时,移除堆顶元素
            minHeap.pop();
        }
    }
    
    return minHeap.top();  // 堆顶元素即为第 k 大的元素
}

示例分析:

  • 输入:nums = [3,2,3,1,2,4,5,5,6]k = 4
  • 目标是找到第4大的元素。
  • 前4个元素 [3, 2, 3, 1] 加入最小堆后,堆为 [1, 2, 3, 3]
  • 接下来遍历剩余的元素:
    • 2,比堆顶 1 大,替换堆顶,堆变为 [2, 2, 3, 3]
    • 4,比堆顶 2 大,替换堆顶,堆变为 [2, 3, 3, 4]
    • 5,比堆顶 2 大,替换堆顶,堆变为 [3, 3, 4, 5]
    • 5,比堆顶 3 大,替换堆顶,堆变为 [3, 4, 5, 5]
    • 6,比堆顶 3 大,替换堆顶,堆变为 [4, 5, 5, 6]
  • 最终堆顶元素 4 就是第4大的元素。

时间复杂度分析:

  • 时间复杂度 O ( n log ⁡ k ) O(n \log k) O(nlogk),每次插入和删除操作的复杂度为 O ( log ⁡ k ) O(\log k) O(logk),共进行 n n n 次操作。
  • 空间复杂度 O ( k ) O(k) O(k),堆中最多有 k 个元素。

两种方法的比较:

方法平均时间复杂度最坏时间复杂度空间复杂度适用场景
快速选择算法 O ( n ) O(n) O(n) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)一次性查找第 k 大元素
最小堆算法 O ( n log ⁡ k ) O(n \log k) O(nlogk) O ( n log ⁡ k ) O(n \log k) O(nlogk) O ( k ) O(k) O(k)需要频繁查找第 k 大元素或处理数据流

总结:

  • 快速选择算法适用于一次性查找,可以在不完全排序的情况下快速找到第 k 大元素,具有较高的平均效率。
  • 最小堆算法适用于需要频繁查找第 k 大元素的场景,尤其在数据流场景下十分高效,能保证每次查找的时间复杂度为 O ( log ⁡ k ) O(\log k) O(logk)

根据实际场景选择合适的方法能更好地解决问题。如果您有任何问题或需要进一步的解释,欢迎讨论!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2189164.html

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

相关文章

老年人意外跌倒感知技术

意外跌倒是导致老年人仙游的6大原因之一&#xff0c;尤其多余80岁以上的老年人。跌倒已成为我国 65 岁以上老年人因伤致死的首位原因&#xff08;来源&#xff1a;IT之家&#xff09;。 跌倒最容易发生在两个地方&#xff0c;卫生间和过道。主要可能是卫生间没有安装扶手&…

关于Generator,async 和 await的介绍

在本篇文章中我们主要围绕下面几个问题来介绍async 和await &#x1f370;Generator的作用&#xff0c;async 及 await 的特点&#xff0c;它们的优点和缺点分别是什么&#xff1f;await 原理是什么&#xff1f; &#x1f4c5;我的感受是我们先来了解Generator&#xff0c;在去…

将视频改成代码滚动

本文章就来讲讲如何将视频转换成代码滚动&#xff0c;也就是这种模式&#xff1a; 本文章就来详细的教大家如何制作达到这种效果吧&#xff01; &#xff08;注&#xff1a;我记得一些python库也可以轻松达到这些效果&#xff0c;但我一时半伙想不起来了&#xff0c;所以这里用…

idea插件开发的第六天-开发一个笔记插件

介绍 Demo说明 本文基于maven项目开发,idea版本为2022.3以上,jdk为1.8本文在JTools插件之上进行开发本插件目标是做一款笔记插件,用于开发者在开发过程中随时记录信息仓库地址: jtools-notes JTools插件说明 Tools插件是一个Idea插件,此插件提供统一Spi规范,极大的降低了id…

手写mybatis之Mapper XML的解析和注册使用

前言 你是怎么面对功能迭代的&#xff1f; 很多程序员在刚开始做编程或者新加入一家公司时&#xff0c;都没有多少机会可以做一个新项目&#xff0c;大部分时候都是在老项目上不断的迭代更新。在这个过程你可能要学习N个前人留下的各式各样的风格迥异的代码片段&#xff0c;在这…

【杂谈一之概率论】CDF、PDF、PMF和PPF概念解释与分析

一、概念解释 1、CDF&#xff1a;累积分布函数&#xff08;cumulative distribution function&#xff09;&#xff0c;又叫做分布函数&#xff0c;是概率密度函数的积分&#xff0c;能完整描述一个实随机变量X的概率分布 2、PDF&#xff1a;连续型概率密度函数&#xff08;p…

平面电磁波的电场能量磁场能量密度相等,能量密度的体积分等于能量,注意电场能量公式也没有复数形式(和坡印廷类似)

1、电场能量密度和磁场能量密度相等(实数场算的) 下面是电场能量密度和磁场能量密度的公式&#xff0c;注意这可不是坡印廷定理。且电场能量密度没有复数表达式&#xff0c;即不是把E和D换成复数形式就行的。注意&#xff0c;一个矢量可以转化为复数形式&#xff0c;两个矢量做…

数据挖掘-padans初步使用

目录标题 Jupyter Notebook安装启动 Pandas快速入门查看数据验证数据建立索引数据选取⚠️注意&#xff1a;排序分组聚合数据转换增加列绘图line 或 **&#xff08;默认&#xff09;&#xff1a;绘制折线图。bar&#xff1a;绘制条形图。barh&#xff1a;绘制水平条形图。hist&…

Discord:报错:A fatal Javascript error occured(解决办法)

按 Windows 键 R 并输入 %appdata% 选择 discord 文件夹并将其删除。 再次按 Windows 键 R 并输入 %LocalAppData% 选择 discord 文件夹并再次将其删除。 附加&#xff1a; 如果还不行&#xff0c;就通过官网下载吧&#xff0c;这个问题通过epic下载可能会有

图文深入理解Oracle DB企业级集中管理神器-GC的安装和部署

值此国庆佳节&#xff0c;深宅家中&#xff0c;闲来无事&#xff0c;就多写几篇博文。今天继续宅继续写。 本文承接上篇&#xff0c;介绍GC的安装和部署。咱们不急&#xff0c;慢慢来&#xff0c;饭要一口一口地吃才能吃得踏实自然。 限于篇幅&#xff0c;本节将重点介绍关键步…

【ubuntu】apt是什么

目录 1.apt简介 2.常用apt指令 2.1安装 2.2更新列表 2.3更新已经安装的软件包 2.4搜索软件包 2.5显示软件包信息 2.6移除软件包 2.7清理无用的安装包 2.8清理无用的依赖项 3.apt和apt-get 3.1区别 3.2 总结 1.apt简介 apt的全称是advanced package …

JAVA的三大特性-封装、继承、多态

Java作为一种面向对象的编程语言&#xff0c;其核心特性包括封装、继承和多态。这三大特性是Java语言的基石&#xff0c;它们相互关联&#xff0c;共同构成了Java强大的面向对象能力。 封装&#xff08;Encapsulation&#xff09; 封装是面向对象编程的一个重要概念&#xff0c…

Pytorch最最适合研究生的入门教程,Q3 开始训练

文章目录 Pytorch最最适合研究生的入门教程Q3 开始训练3.1 训练的见解3.2 Pytorch基本训练框架work Pytorch最最适合研究生的入门教程 Q3 开始训练 3.1 训练的见解 如何理解深度学习能够完成任务&#xff1f; 考虑如下回归问题 由函数 y f ( x ) yf(x) yf(x)采样得到的100个…

现在的新电脑在任务管理器里又多了个NPU?它是啥?

前言 今年中旬各家品牌的新笔记本感觉上都是很不错&#xff0c;搞得小白自己心痒痒&#xff0c;突然间想要真的买一台Windows笔记本来耍耍了。 但今天这个文章并不是什么商品宣传啥的&#xff0c;而是小白稍微尝试了一下新笔记本之后的一些发现。 在今年的新笔记本上都多了一…

【GESP】C++一级练习BCQM3025,输入-计算-输出-6

题型与BCQM3024一样&#xff0c;计算逻辑上稍微复杂了一点点&#xff0c;代码逻辑没变&#xff0c;仍属于小学3&#xff0c;4年级的题目水平。 题解详见&#xff1a;https://www.coderli.com/gesp-1-bcqm3025/ https://www.coderli.com/gesp-1-bcqm3025/https://www.coderli.c…

数据提取之JSON与JsonPATH

第一章 json 一、json简介 json简单说就是javascript中的对象和数组&#xff0c;所以这两种结构就是对象和数组两种结构&#xff0c;通过这两种结构可以表示各种复杂的结构 > 1. 对象&#xff1a;对象在js中表示为{ }括起来的内容&#xff0c;数据结构为 { key&#xff1…

最新版本SkyWalking【10.1.0】部署

这里写目录标题 前言前置条件启动Skywalking下载解压启动说明 集成Skywalking Agent下载Agent在IDEA中添加agent启动应用并访问SpringBoot接口 说明 前言 基于当前最新版10.1.0搭建skywalking 前置条件 装有JDK11版本的环境了解SpringBoot相关知识 启动Skywalking 下载 地…

浑元换算策略和武德换算策略-《分析模式》漫谈36

DDD领域驱动设计批评文集 做强化自测题获得“软件方法建模师”称号 《软件方法》各章合集 “Analysis Patterns”的第3章有这么一句&#xff1a; A conversion, however deterministic, does not follow that faithfully. 2004&#xff08;机械工业出版社&#xff09;中译本…

HTB:Explosion[WriteUP]

目录 连接至HTB服务器并启动靶机 1.What does the 3-letter acronym RDP stand for? 2.What is a 3-letter acronym that refers to interaction with the host through a command line interface? 3.What about graphical user interface interactions? 4.What is the…

【MySQL 08】复合查询

目录 1.准备工作 2.多表查询 笛卡尔积 多表查询案例 3. 自连接 4.子查询 1.单行子查询 2.多行子查询 3.多列子查询 4.在from子句中使用子查询 5.合并查询 1.union 2.union all 1.准备工作 如下三个表&#xff0c;将作为示例&#xff0c;理解复合查询 EMP员工表…