数组中的第K大元素问题


问题: 在未排序的数组中找到第 k 个最大的元素。请注意,需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
约定: 假设这里数组的长度为 n。

方法一:基于快速排序的选择方法

思路和方法
可以用快速排序来解决这个问题,先对原数组排序,再返回倒数第K个位置,这样的平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),但其实我们可以做的更快。
首先回顾快速排序,这是一个典型的分支算法。对数组 a [ l . . . r ] a[l...r] a[l...r]做快速排序的过程:

  • 分解: 将数组 a [ l . . . r ] a[l...r] a[l...r] 「划分」 成两个子数组 a [ l . . . q − 1 ] 、 a [ q + 1... r ] a[l...q-1]、a[q+1...r] a[l...q−1]、a[q+1...r],使得 a [ l . . . q − 1 ] a[l...q-1] a[l...q−1]中的每个元素都小于a[q],且a[q]小于等于 a [ q + 1... r ] a[q+1...r] a[q+1...r]中的每个元素。其中,计算下标q也是「划分」过程的一部分。
  • 解决: 通过递归调用快速排序,对子数组 a [ l . . . q − 1 ] a[l...q-1] a[l...q−1]和 a [ q + 1... r ] a[q+1...r] a[q+1...r]进行排序。
  • 合并: 因为子数组都是原址排序的,所以并不需要进行合并操作, a [ l . . . r ] a[l...r] a[l...r]已经有序。
  • 上文提到的 「划分」 过程是:从子数组 a [ l . . . r ] a[l...r] a[l...r]中选择任意一个元素 x作为主元,调整子数组的元素使得左边的元素都小于等于它,右边的元素都大于等于它,x的最终位置就是q。

由此发现每次经过「划分」操作后,一定可以确定一个元素的最终位置,即x的最终位置为 q,并且保证 a [ l . . . q − 1 ] a[l...q-1] a[l...q−1]中的每个元素小于等于a[q],且a[q] 小于等于 a [ q + 1... r ] a[q+1...r] a[q+1...r]中的每个元素。所以只要某次划分的q为倒数第k个下标的时候,就找到了答案。 只关心这一点,至于 a [ l . . . q − 1 ] a[l...q-1] a[l...q−1]和 a [ q + 1... r ] a[q+1...r] a[q+1...r]是否是有序的,不关心。
因此可以改进快速排序算法来解决这个问题:在分解的过程当中,会对子数组进行划分,如果划分得到的q正好就是我们需要的下标,就直接返回a[q];否则,如果q比目标下标小,就递归右子区间,否则递归左子区间。这样就可以把原来递归两个区间变成只递归一个区间,提高了时间效率。这就是「快速选择」算法。

我们知道快速排序的性能和「划分」出的子数组的长度密切相关。直观地理解如果每次规模为n的问题我们都划分成1和 n − 1 n - 1 n−1,每次递归的时候又向 n − 1 n - 1 n−1的集合中递归,这种情况是最坏的,时间代价是 O ( n 2 ) O(n ^ 2) O(n2)。这里可以引入随机化来加速这个过程,它的时间代价的期望是 O ( n ) O(n) O(n),证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。

int partition(vector<int>& a, int l, int r) {// 3. 在调用当前方法的randomPartition方法中,已经确定了了随机数是a[r]int x = a[r], i = l - 1;// 首先比较区间在[l, r]之间, 所以a[j]中的    l<= j <= rfor (int j = l; j < r; ++j) {// 4. a[j] 跟随机数 x 比较, 小于x的数都跟[l,r]左边区间交换,i=l-1,所以++i=l,初始索引就是l,if (a[j] <= x) {swap(a[++i], a[j]);  //两两交换}}// 这个for循环操作就是将小于 x 的数都往[i, j]的左边区间设置,从而实现存在[l, i]区间,使得对应数值都 小于 x//5. 既然已经将<x的值都放在一边了,现在将x也就是a[r] 跟a[i+1]交换,从而分成两个区间[l.i+1]左, [i+2, r]右,左边区间的值都小于xswap(a[i + 1], a[r]);// 然后返回这个分区值return i + 1;
}int randomPartition(vector<int>& a, int l, int r) {// 1. 随机数范围: [0, r-l+1) 同时加l, 则是 [l, r+1) = [l, r] 也就是在这个[l,r] 中随机选一个索引出来int i = rand() % (r - l + 1) + l;// 2. 交换a[i], a[r], 也就是将随机数先放在[l,r]最右边a[r]上swap(a[i], a[r]);return partition(a, l, r);
}// 得到分区值索引q
int quickSelect(vector<int>& a, int l, int r, int index) {int q = randomPartition(a, l, r);// 如果刚好索引q就是想要的索引,则直接返回if (q == index) {return a[q];} else {// 如果不是,比较q 与 index ,确定下次要检索的区间, 要么是[q+1, right], 要么就是[left, q-1]return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}// 要找到的元素所在索引:  前K大,即倒数索引第K个
int findKthLargest(vector<int>& nums, int k) {srand(time(0));return quickSelect(nums, 0, nums.size() - 1, nums.size() - k);
}

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),如上文所述,证明过程可以参考「《算法导论》9.2:期望为线性的选择算法」。
  • 空间复杂度: O ( l o g n ) O(logn) O(logn),递归使用栈空间的空间代价的期望为 O ( l o g n ) O(logn) O(logn)。

库函数

nth_element(first, nth, last, compare)

求[first, last]这个区间中第n大小的元素,如果参数加入了compare函数,就按compare函数的方式比较。nth_element仅排序第n个元素(从0开始索引),即将位置n(从0开始)的元素放在第n大的位置,处理完之后,默认排在它前面的元素都不比它大,排在它后面的元素都不比它小。


方法二:基于堆排序的选择方法

思路和算法

也可以使用堆排序来解决这个问题——建立一个大根堆,做 k − 1 k - 1 k−1次删除操作后堆顶元素就是我们要找的答案。在很多语言中,都有优先队列或者堆的的容器可以直接使用,但是在面试中,面试官更倾向于让更面试者自己实现一个堆。所以这里实现大根堆,主要搞懂「建堆」、「调整」和「删除」的过程。

堆是一种特殊的树结构,即完全二叉树。堆分为大根堆和小根堆,大根堆为根节点的值大于两个子节点的值;小根堆为根节点的值小于两个子节点的值,同时根节点的两个子树也分别是一个堆。

通俗来讲,二叉树在按层序遍历时在遇到第一个NULL指针即作为结尾的二叉树就可以称之为完全二叉树。

根节点下标为0,若父节点相应数组下标为i,则其左孩子相应数组下标为2i+1,右孩子为2i+2.

注意:如果我们现在处理第i个序号的结点的数,那么他的父结点序号就是 ( i − 1 ) / 2 (i-1)/2 (i−1)/2,它的孩子结点就为 2 i + 1 2i+1 2i+1与 2 i + 2 2i+2 2i+2。这个容易想,右子节点如果直接除以2,得到 i + 1 i+1 i+1,但是 2 i + 2 − 1 2i+2-1 2i+2−1和 2 i + 1 − 1 2i+1-1 2i+1−1除以2可以得到i。根节点从0开始,则n个节点的堆,最后一个叶节点的下标为 n − 1 n-1 n−1,其父节点是第一个非叶节点,下标为 ( ( n − 1 ) − 1 ) / 2 = ( n − 2 ) / 2 = n / 2 − 1 ((n-1)-1 )/ 2 = (n-2)/2 = n/2 - 1 ((n−1)−1)/2=(n−2)/2=n/2−1

  1. 首先用前n个元素的无序序列,构建成大顶堆;构建大顶堆时,从最后一个非叶节点 n / 2 − 1 n/2-1 n/2−1的位置开始检查节点与其孩子值是否满足大顶堆的要求,不满足则需要调整该元素与其孩子节点元素的位置,如果有调整,则调整过的孩子节点(子树)也要递归调用调整子树中的元素值位置,保证子树也是大顶堆。然后按照层次遍历的顺序依次往前,从右到左,从下到上调整所有非叶节点的值,最后根节点的值就是最大值;
  2. 得到大顶堆后将根节点与数组待排序部分的最后一个元素交换位置,即将最大元素"沉"到数组末端;
  3. 交换过后待排序数组长度减一,再对新长度的待排序数组重复上述过程,直到整个数组排序完成。如果我们要数组整体递增有序,则每次构建的是大顶堆;如果我们要数组整体递减有序,则每次构建的是小顶堆。
void buildMaxHeap(vector<int>& A, int n)  //建立最大堆{
// 从最后一个非叶子节点(n/2-1)开始自底向上构建,for(int i = n/2-1; i>=0; --i)  //从(n/2-1)调用一次maxHeapIfy就可以得到最大堆maxHeapify(A, i, n);}void maxHeapify(vector<int> &A, int i, int n) //将i节点为根的堆中小的数依次上移,n表示堆中的数据个数
{int left = 2*i+1;  //i的左儿子int right = 2*i+2;  //i的右儿子int largest = i;     //先设置父节点和子节点三个节点中最大值的位置为父节点下标if(left < n && A[left] > A[largest]){largest = left;}if(right < n && A[right] > A[largest]){largest = right;}if(largest != i){ //最大值不是父节点,交换swap(A[i], A[largest]);maxHeapify(A, largest, n); //递归调用,保证子树也是最大堆}
}void heapSort(vector<int>& A, int n){   // 堆排序算法buildMaxHeap(A,n);  //先建立堆for(int i = n-1; i>0; --i){// 将根节点(最大值)与数组待排序部分的最后一个元素交换,这样最终得到的是递增序列swap(A[0], A[i]);// 待排序数组长度减一,只要对换到根节点的元素进行排序,将它下沉就好了。maxHeapify(A, 0, i);}
}int findKthLargest(vector<int>& nums, int k) {int heapSize = nums.size();buildMaxHeap(nums, heapSize);for (int i = nums.size() - 1; i >= nums.size() - k + 1; --i) {swap(nums[0], nums[i]);--heapSize;maxHeapify(nums, 0, heapSize);}return nums[0];
}

复杂度分析

  • 时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn),建堆的时间代价是 O ( n ) O(n) O(n),删除的总代价是 O ( k l o g n ) O(klogn) O(klogn),因为 k < n k < n k<n,故渐进时间复杂为 O ( n + k l o g n ) = O ( n l o g n ) O(n+klogn)=O(nlogn) O(n+klogn)=O(nlogn)。
  • 空间复杂度: O ( l o g n ) O(logn) O(logn),即递归使用栈空间的空间代价。

优先级队列

priority_queue<Type, Container, Functional>

Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。

如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator<,也就是优先队列是大根堆,队头元素最大。

小根堆用greater <Type>函数,下面是小根堆的例子

priority_queue<int, vector<int>, greater<int> > p;

堆排序中,用优先级队列涉及到了自定义排序的问题,这里展示了两种写法:

  • 使用 lambda表达式 + decltype
  • 使用 struct + 重载操作符()
//lambda表达式
auto cmp = [捕获列表](参数列表) -> 返回类型{ 函数体};
priority_queue<Type, Container, decltype(cmp)> pq(cmp);//结构体,重载操作符()
struct cmp{bool operator()(参数列表) {return ;}
}

数组中的第K大元素问题(C++)相关推荐

  1. 【算法】快速选择算法 ( 数组中找第 K 大元素 )

    算法 系列博客 [算法]刷题范围建议 和 代码规范 [算法]复杂度理论 ( 时间复杂度 ) [字符串]最长回文子串 ( 蛮力算法 ) [字符串]最长回文子串 ( 中心线枚举算法 ) [字符串]最长回文 ...

  2. 如何寻找无序数组中的第K大元素?

    如何寻找无序数组中的第K大元素? 有这样一个算法题:有一个无序数组,要求找出数组中的第K大元素.比如给定的无序数组如下所示: 如果k=6,也就是要寻找第6大的元素,很显然,数组中第一大元素是24,第二 ...

  3. DC-leetcode215数组中的第k大元素

    在未排序的数组中找到第 k 个最大的元素.请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素. 示例 1: 输入: [3,2,1,5,6,4] 和 k = 2 输出: 5 ...

  4. 网易_在数组中查找前K个元素

    笔试题,最后一题 查找网易云音乐中播放量最大的前K个歌曲. 换句话说,就是在数组中查找前K大元素. 大致有以下几个思路. 1.第一感觉就是对数组进行降序全排序,然后返回前K个元素,即是需要的K个最大数 ...

  5. 无序数组中找第K大的数

    类快排算法 leetcode215 由于只要求找出第k大的数,没必要将数组中所有值都排序. 典型解法:快速排序分组. 在数组中找到第k大的元素 取基准元素,将元素分为两个集合,一个集合元素比基准小,另 ...

  6. 从C语言的角度重构数据结构系列(七)-数据结构堆知识求解数据流中的第K大元素

    前言 在这里给自己打个广告,需要的小伙伴请自行订阅. python快速学习实战应用系列课程 https://blog.csdn.net/wenyusuran/category_2239261.html ...

  7. 915. 分割数组、剑指 Offer II 076. 数组中的第 k 大的数字

    LeetCode题解 1.分割数组 2.数组中的第 k 大的数字 1.分割数组 题目描述: ➡️挑战链接⬅️ 分析: 首先题目叙述的很简单: 要求呢 1.左右两个区间元素必须连续 2.左右区间必须都有 ...

  8. 【LeetCode】快排-无序整数数组中找第k大的数(或者最小的k个数)

    一个有代表性的题目:无序整数数组中找第k大的数,对快排进行优化. 这里先不说这个题目怎么解答,先仔细回顾回顾快排,掰开了揉碎了理解理解这个排序算法:时间复杂度.空间复杂度:什么情况下是复杂度最高的情况 ...

  9. 1985. 找出数组中的第 K 大整数

    1985. 找出数组中的第 K 大整数 给你一个字符串数组 nums 和一个整数 k .nums 中的每个字符串都表示一个不含前导零的整数. 返回 nums 中表示第 k 大整数的字符串. 注意:重复 ...

最新文章

  1. java for 跳过_在for循环中跳过错误
  2. 从数据集到2D和3D方法,一文概览目标检测领域进展
  3. 随机森林(Random Forest)为什么是森林?到底随机在哪里?行采样和列采样又是什么东西?
  4. Learning To Rank之LambdaMART的前世今生
  5. 职责链模式 php,php Chain of Responsibility 职责链模式
  6. java 中sun.net.ftp_开发FTP不要使用sun.net.ftp.ftpClient
  7. Spring框架初写
  8. java ee 中文乱码的问题
  9. Graphviz从入门到不精通
  10. 漫步最优化四十一——Powell法(下)
  11. IOS 判断设备屏幕尺寸、分辨率
  12. python安装tensorflow失败解决办法_pip安装tensorflow总是失败怎么办?
  13. Channel~scatter and gather
  14. linux 服务器远程开机,详解使用Ubuntu系统中实现远程开机的方法
  15. 企业管理理论综述与实践 — 绩效
  16. NLP 分类问题的讨论
  17. Android 双卡双待识别
  18. 渗透测试-信息搜集的目的和方法
  19. mac 更换jupyter的默认启动浏览器
  20. 钉钉开放平台API对接第一讲

热门文章

  1. win10计算机快捷方式怎么弄到桌面,win10怎么把此电脑放到桌面_w10如何把此电脑添加到桌面...
  2. 李玉婷mysql2019_学习记录-第五天(李玉婷MySQL基础 第1天)
  3. 计算机教学辅助平台,教学辅助平台
  4. Encoder-Decoder LSTM Model模型对家庭用电进行多步时间序列预测
  5. SPI接口原理与配置
  6. 【原创分析帖】据说Google内部有史以来最难的一道面试题
  7. 2022年前端软件开发培训学校排名
  8. 电脑技巧常识, 微信技巧, 快捷键
  9. eNSP安装包以及镜像包(设备包)全集
  10. [JZOJ5336] 提米树