后台回复进群一起刷力扣????

点击下方卡片可搜索文章????

读完本文,可以去力扣解决如下题目:

215.数组中的第 K 个最大元素(Medium

快速选择算法是一个非常经典的算法,和快速排序算法是亲兄弟。

原始题目很简单,给你输入一个无序的数组nums和一个正整数k,让你计算nums中第k大的元素。

那你肯定说,给nums数组排个序,然后取第k个元素,也就是nums[k-1],不就行了吗?

当然可以,但是排序时间复杂度是O(NlogN),其中N表示数组nums的长度。

我们就想要第k大的元素,却给整个数组排序,有点杀鸡用牛刀的感觉,所以这里就有一些小技巧了,可以把时间复杂度降低到O(NlogK)甚至是O(N),下面我们就来具体讲讲。

力扣第 215 题「数组中的第 K 个最大元素」就是一道类似的题目,函数签名如下:

int findKthLargest(int[] nums, int k);

只不过题目要求找k个最大的元素,和我们刚才说的k大的元素在语义上不太一样,题目的意思相当于是把nums数组降序排列,然后返回第k个元素。

比如输入nums = [2,1,5,4], k = 2,算法应该返回 4,因为 4 是nums中第 2 个最大的元素。

这种问题有两种解法,一种是二叉堆(优先队列)的解法,另一种就是标题说到的快速选择算法(Quick Select),我们分别来看。

二叉堆解法

二叉堆的解法比较简单,实际写算法题的时候,推荐大家写这种解法,先直接看代码吧:

int findKthLargest(int[] nums, int k) {// 小顶堆,堆顶是最小元素PriorityQueue<Integer> pq = new PriorityQueue<>();for (int e : nums) {// 每个元素都要过一遍二叉堆pq.offer(e);// 堆中元素多于 k 个时,删除堆顶元素if (pq.size() > k) {pq.poll();}}// pq 中剩下的是 nums 中 k 个最大元素,// 堆顶是最小的那个,即第 k 个最大元素return pq.peek();
}

二叉堆(优先队列)是比较常见的数据结构,可以认为它会自动排序,我们前文 手把手实现二叉堆数据结构 实现过这种结构,我就默认大家熟悉它的特性了。

看代码应该不难理解,可以把小顶堆pq理解成一个筛子,较大的元素会沉淀下去,较小的元素会浮上来;当堆大小超过k的时候,我们就删掉堆顶的元素,因为这些元素比较小,而我们想要的是前k个最大元素嘛。当nums中的所有元素都过了一遍之后,筛子里面留下的就是最大的k个元素,而堆顶元素是堆中最小的元素,也就是「第k个最大的元素」。

二叉堆插入和删除的时间复杂度和堆中的元素个数有关,在这里我们堆的大小不会超过k,所以插入和删除元素的复杂度是O(logK),再套一层 for 循环,总的时间复杂度就是O(NlogK)。空间复杂度很显然就是二叉堆的大小,为O(K)

这个解法算是比较简单的吧,代码少也不容易出错,所以说如果笔试面试中出现类似的问题,建议用这种解法。唯一注意的是,Java 的PriorityQueue默认实现是小顶堆,有的语言的优先队列可能默认是大顶堆,可能需要做一些调整。

快速选择算法

快速选择算法比较巧妙,时间复杂度更低,是快速排序的简化版,一定要熟悉思路

我们先从快速排序讲起。

快速排序的逻辑是,若要对nums[lo..hi]进行排序,我们先找一个分界点p,通过交换元素使得nums[lo..p-1]都小于等于nums[p],且nums[p+1..hi]都大于nums[p],然后递归地去nums[lo..p-1]nums[p+1..hi]中寻找新的分界点,最后整个数组就被排序了。

快速排序的代码如下:

/* 快速排序主函数 */
void sort(int[] nums) {// 一般要在这用洗牌算法将 nums 数组打乱,// 以保证较高的效率,我们暂时省略这个细节sort(nums, 0, nums.length - 1);
}/* 快速排序核心逻辑 */
void sort(int[] nums, int lo, int hi) {if (lo >= hi) return;// 通过交换元素构建分界点索引 pint p = partition(nums, lo, hi);// 现在 nums[lo..p-1] 都小于 nums[p],// 且 nums[p+1..hi] 都大于 nums[p]sort(nums, lo, p - 1);sort(nums, p + 1, hi);
}

关键就在于这个分界点索引p的确定,我们画个图看下partition函数有什么功效:

索引p左侧的元素都比nums[p]小,右侧的元素都比nums[p]大,意味着这个元素已经放到了正确的位置上,回顾快速排序的逻辑,递归调用会把nums[p]之外的元素也都放到正确的位置上,从而实现整个数组排序,这就是快速排序的核心逻辑。

那么这个partition函数如何实现的呢?看下代码:

int partition(int[] nums, int lo, int hi) {if (lo == hi) return lo;// 将 nums[lo] 作为默认分界点 pivotint pivot = nums[lo];// j = hi + 1 因为 while 中会先执行 --int i = lo, j = hi + 1;while (true) {// 保证 nums[lo..i] 都小于 pivotwhile (nums[++i] < pivot) {if (i == hi) break;}// 保证 nums[j..hi] 都大于 pivotwhile (nums[--j] > pivot) {if (j == lo) break;}if (i >= j) break;// 如果走到这里,一定有:// nums[i] > pivot && nums[j] < pivot// 所以需要交换 nums[i] 和 nums[j],// 保证 nums[lo..i] < pivot < nums[j..hi]swap(nums, i, j);}// 将 pivot 值交换到正确的位置swap(nums, j, lo);// 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]return j;
}// 交换数组中的两个元素
void swap(int[] nums, int i, int j) {int temp = nums[i];nums[i] = nums[j];nums[j] = temp;
}

熟悉快速排序逻辑的读者应该可以理解这段代码的含义了,这个partition函数细节较多,上述代码参考《算法4》,是众多写法中最漂亮简洁的一种,所以建议背住,这里就不展开解释了。

好了,对于快速排序的探讨到此结束,我们回到一开始的问题,寻找第k大的元素,和快速排序有什么关系?

注意这段代码:

int p = partition(nums, lo, hi);

我们刚说了,partition函数会将nums[p]排到正确的位置,使得nums[lo..p-1] < nums[p] < nums[p+1..hi]

那么我们可以把pk进行比较,如果p < k说明第k大的元素在nums[p+1..hi]中,如果p > k说明第k大的元素在nums[lo..p-1]

所以我们可以复用partition函数来实现这道题目,不过在这之前还是要做一下索引转化:

题目要求的是「第k个最大元素」,这个元素其实就是nums升序排序后「索引」为len(nums) - k的这个元素。

这样就可以写出解法代码:

int findKthLargest(int[] nums, int k) {int lo = 0, hi = nums.length - 1;// 索引转化k = nums.length - k;while (lo <= hi) {// 在 nums[lo..hi] 中选一个分界点int p = partition(nums, lo, hi);if (p < k) {// 第 k 大的元素在 nums[p+1..hi] 中lo = p + 1;} else if (p > k) {// 第 k 大的元素在 nums[lo..p-1] 中hi = p - 1;} else {// 找到第 k 大元素return nums[p];}}return -1;
}

这个代码框架其实非常像我们前文 二分搜索框架 的代码,这也是这个算法高效的原因,但是时间复杂度为什么是O(N)呢?按理说类似二分搜索的逻辑,时间复杂度应该一定会出现对数才对呀?

其实这个O(N)的时间复杂度是个均摊复杂度,因为我们的partition函数中需要利用 双指针技巧 遍历nums[lo..hi],那么总共遍历了多少元素呢?

最好情况下,每次p都恰好是正中间(lo + hi) / 2,那么遍历的元素总数就是:

N + N/2 + N/4 + N/8 + … + 1

这就是等比数列求和公式嘛,求个极限就等于2N,所以遍历元素个数为2N,时间复杂度为O(N)

但我们其实不能保证每次p都是正中间的索引的,最坏情况下p一直都是lo + 1或者一直都是hi - 1,遍历的元素总数就是:

N + (N - 1) + (N - 2) + … + 1

这就是个等差数列求和,时间复杂度会退化到O(N^2)为了尽可能防止极端情况发生,我们需要在算法开始的时候对nums数组来一次随机打乱

int findKthLargest(int[] nums, int k) {// 首先随机打乱数组shuffle(nums);// 其他都不变int lo = 0, hi = nums.length - 1;k = nums.length - k;while (lo <= hi) {// ...}return -1;
}// 对数组元素进行随机打乱
void shuffle(int[] nums) {int n = nums.length;Random rand = new Random();for (int i = 0 ; i < n; i++) {// 从 i 到最后随机选一个元素int r = i + rand.nextInt(n - i);swap(nums, i, r);}
}

前文 洗牌算法详解 写过随机乱置算法,这里就不展开了。当你加上这段代码之后,平均时间复杂度就是O(N)了,提交代码后运行速度大幅提升。

总结一下,快速选择算法就是快速排序的简化版,复用了partition函数,快速定位第 k 大的元素。相当于对数组部分排序而不需要完全排序,从而提高算法效率,将平均时间复杂度降到O(N)。

往期推荐 ????

BFS 算法框架套路详解

回溯算法最佳实践:合法括号生成

学会这些 shell 小技巧,我就爱上 Linux 了

详解最长公共子序列问题,秒杀三道动态规划题目

_____________

学好算法靠套路,认准 labuladong,知乎、B站账号同名。

《labuladong的算法小抄》即将出版,公众号后台回复关键词「pdf」下载,回复「进群」可加我好友,拉你进刷题群。

快排亲兄弟:快速选择算法详解相关推荐

  1. 基础排序算法详解与优化

    文章图片存储在GitHub,网速不佳的朋友,请看<基础排序算法详解与优化> 或者 来我的技术小站 godbmw.com 1. 谈谈基础排序 常见的基础排序有选择排序.冒泡排序和插入排序.众 ...

  2. YOLOv5算法详解

    目录 1.需求解读 2.YOLOv5算法简介 3.YOLOv5算法详解 3.1 YOLOv5网络架构 3.2 YOLOv5实现细节详解 3.2.1 YOLOv5基础组件 3.2.2 输入端细节详解 3 ...

  3. 7大排序算法详解+java实现

    目录 0 概述 1 冒泡排序 2 选择排序 3 插入排序 4 希尔排序 5 快速排序 6 归并排序 7 基数排序 下载地址 7大排序算法详解文档及java代码实现(可直接运行)下载地址:https:/ ...

  4. js排序算法详解-快速排序

    全栈工程师开发手册 (作者:栾鹏) js系列教程5-数据结构和算法全解 js排序算法详解-快速排序 既然是快速排序,那顾名思义一定很快,快的连小编都被懵逼了好几圈!建议先不要看动图,先看第一种写法: ...

  5. 排序算法,最全的10大排序算法详解(Sort Algorithm)

    文章目录 排序算法,最全的10大排序算法详解(Sort Algorithm) 排序算法分类 排序算法稳定性 时间复杂度(time complexity) 1#时间复杂度的意义 2#基本操作执行次数 如 ...

  6. 算法-详解堆排序算法

    title: 算法-详解堆排序算法 date: 2017-07-06 22:00:16 categories: 算法,面试 tags: [算法,Algorithm,面试,排序] description ...

  7. JS 排序算法详解(冒泡排序,选择排序,插入排序,希尔排序,快速排序)

    JS 排序算法详解(冒泡排序,选择排序,插入排序,希尔排序,快速排序) 一. 大O表示法 在进行排序算法之前,我们得先掌握一种对算法效率的表示方法,大O表示法. 我们使用大O表示法来表示算法的时间复杂 ...

  8. 快速排序算法详解(原理,时间复杂度,实现代码)

    快速排序算法详解(原理.实现和时间复杂度) 快速排序是对冒泡排序的一种改进,由 C.A.R.Hoare(Charles Antony Richard Hoare,东尼·霍尔)在 1962 年提出. 快 ...

  9. YOLOv4算法详解

    YOLOv4: Optimal Speed and Accuracy of Object Detection-论文链接-代码链接 目录 1.需求解读 2.YOLOv4算法简介 3.YOLOv4算法详解 ...

最新文章

  1. client-go入门之1:创建连接Kubernetes集群的客户端
  2. streaming api_通过Spring Integration消费Twitter Streaming API
  3. jquery 学习之一 对象访问
  4. 前端学习(1907)vue之电商管理系统电商系统之渲染修改用户的表单的重置操作
  5. 作者:包阳(1978-),男,北京系统工程研究所副研究员。
  6. 基于asp.net的音乐分享网站的设计与实现(含源文件)
  7. 苹果4是android吗,时至2020年,苹果手机还有这几个优势,让安卓毫无“招架之力”...
  8. 2021年中国动物血浆制品及其衍生物市场趋势报告、技术动态创新及2027年市场预测
  9. ddr老化测试_塑胶类材料老化测试(Aging Test )常用的测试标准
  10. 使用paramiko在eNSP的交换机中批量创建VLAN
  11. 对话周鸿祎:从程序员创业谈起
  12. leetcode刷题日记-leetcode刷题日记-71. 简化路径
  13. 基于SSM的教师本科教学质量评价管理系统
  14. matlab信号加入白噪音再分离,Matlab中给信号增加白噪声
  15. win10连接共享打印机出现报错0x0000007c问题
  16. element-ui安装失败解决方法
  17. 毕业论文写作与学术规范
  18. 线性代数 --- Gauss消元的部分主元法和完全主元法(补充)
  19. WinMerge 过滤器用法
  20. nginx 搭建静态网站

热门文章

  1. 以每月5美元的价格建立自己的博客[分步指南]
  2. CVPR 2022 | 阿里达摩院提出ABPN:高清人像美肤模型
  3. 微信公众号开发教程(一) 验证接入
  4. Keycloak的搭建
  5. Redis持久化的两种方式
  6. JS JSON Ajax
  7. jacoco统计覆盖率最佳实践
  8. 【python】实例属性的显示方法-dir、__dict__
  9. 蓝桥杯试题 算法提高 书院主持人
  10. 常用IDE/编辑器 格式化代码快捷键