0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

算法与数据结构 来源:labuladong 作者:labuladong 2020-12-02 16:00 次阅读

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

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

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

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

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

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

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

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

intfindKthLargest(int[]nums,intk);

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

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

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

二叉堆解法

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

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

看代码应该不难理解,可以把小顶堆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]中寻找新的分界点,最后整个数组就被排序了。

快速排序的代码如下:

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

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

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

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

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

注意这段代码:

intp=partition(nums,lo,hi);

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

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

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

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

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

这个代码框架其实非常像我们前文二分搜索框架的代码,这也是这个算法高效的原因,但是时间复杂度为什么是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数组来一次随机打乱:

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

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

责任编辑:xj

原文标题:快排亲兄弟:快速选择算法详解

文章出处:【微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 算法
    +关注

    关注

    23

    文章

    4455

    浏览量

    90756
  • 代码
    +关注

    关注

    30

    文章

    4555

    浏览量

    66771

原文标题:快排亲兄弟:快速选择算法详解

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    深入探索KUKA KRL中的数组应用

    如果 CHAR 类型数组的所有数组元素都拥有相同的字符串,则不必单独初始化每个数组元素。忽略右侧的数组
    的头像 发表于 04-18 10:37 137次阅读
    深入探索KUKA KRL中的<b class='flag-5'>数组</b>应用

    随机抽取SV数组中的一个元素方法实现

    如果想从一个关联数组中随机选取一个元素,需要逐个访问它之前的元素,原因是没办法能够直接访问到第N个元素。上面的程序示范了如何从一个以整数值作为索引**的关联
    的头像 发表于 03-21 10:11 153次阅读
    随机抽取SV<b class='flag-5'>数组</b>中的一个<b class='flag-5'>元素</b>方法实现

    PHP中数组的使用方法!

    如何创建数组、添加/删除元素、访问数组元素、遍历数组以及使用数组方法和函数等等。 首先,我们来看
    的头像 发表于 01-12 15:11 163次阅读

    labview怎么查数组中相同元素的个数

    要查找LabVIEW中数组中相同元素的个数,可以使用以下步骤: 创建一个包含要查找的数值的数组。这可以通过手动输入数组元素或从文件/其他数据
    的头像 发表于 12-28 16:42 1026次阅读

    C语言中的数组格式与初始化

        数组:只能存放一种数据类型,比如int类型的数组、float类型的数组,里面存放的数据称为“元素”。     数组的定义:     
    的头像 发表于 11-26 16:12 478次阅读
    C语言中的<b class='flag-5'>数组</b>格式与初始化

    C语言中数组的用法

    C语言的数组是一种数据结构,它可以存储多个相同类型的数据,例如整数,字符,浮点数等。数组的每个元素都有一个索引,用来表示它在数组中的位置。数组
    的头像 发表于 11-24 17:48 843次阅读
    C语言中<b class='flag-5'>数组</b>的用法

    c语言中数组怎么定义

    C语言中,数组是一种用来存储相同类型元素的数据结构。它可以存储多个元素,并通过一个共同的名称来引用这些元素数组是一种很重要的数据结构,可以
    的头像 发表于 11-24 10:11 1088次阅读

    c语言在数组中查找指定元素

    C语言是一种通用的编程语言,广泛应用于各种领域,包括嵌入式系统、操作系统、游戏开发等。在C语言中,数组是一种非常重要的数据结构,用于存储一系列相同类型的元素。查找指定元素数组中是否存
    的头像 发表于 11-24 10:07 1364次阅读

    什么是数组数组有什么用?数组的使用方法

    数组(Array)是有序的元素序列。
    的头像 发表于 11-08 14:58 706次阅读
    什么是<b class='flag-5'>数组</b>?<b class='flag-5'>数组</b>有什么用?<b class='flag-5'>数组</b>的使用方法

    用二维数组存储最小元素是怎么实现的?

    用二维数组存储最小元素是怎么实现的?  在计算机编程中,数组是一种非常重要的数据结构之一。它用于存储一系列相同类型的变量,这些变量被放入单一的数组中并通过一个唯一的编号(称为
    的头像 发表于 09-13 11:17 315次阅读

    C语言—二维数组介绍

    定义一个两行三列的二维数组,总共有6个元素
    的头像 发表于 09-11 14:51 553次阅读
    C语言—二维<b class='flag-5'>数组</b>介绍

    C++数组名和数组拷贝详解

    C++数组间赋值不能直接通过数组名称 randy = sesame进行,因为数组名并不是指针,大部分情况下,编译器会隐式转换为指向数组元素
    发表于 08-21 15:09 299次阅读
    C++<b class='flag-5'>数组</b>名和<b class='flag-5'>数组</b>拷贝详解

    动态数组和C++ std::vector详解

    std::vector是C++的默认动态数组,其与array最大的区别在于vector的数组是动态的,即其大小可以在运行时更改。std::vector是封装动态数组的顺序容器,且该容器
    的头像 发表于 07-19 11:07 715次阅读

    C 语言数组的基本结构

    元素数组元素的最短距离 求两个有序数组的共同元素 求三个数组的共同
    的头像 发表于 06-22 10:56 384次阅读

    定义了一64K字节的数组,如果把数组定义成变量数组运行不了怎么解决?

    我定义了一64K字节的数组,如果把数组定义成Const常量,编辑之后运行都正常。 但如果把数组定义成变量
    发表于 06-13 08:36