You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
letfindKthLargest=function(nums,k){returnquickSelect(nums,nums.length-k)};letquickSelect=(arr,k)=>{returnquick(arr,0,arr.length-1,k)}letquick=(arr,left,right,k)=>{letindexif(left<right){// 划分数组index=partition(arr,left,right)// Top kif(k===index){returnarr[index]}elseif(k<index){// Top k 在左边returnquick(arr,left,index-1,k)}else{// Top k 在右边returnquick(arr,index+1,right,k)}}returnarr[left]}letpartition=(arr,left,right)=>{// 取中间项为基准vardatum=arr[Math.floor(Math.random()*(right-left+1))+left],i=left,j=right// 开始调整while(i<j){// 左指针右移while(arr[i]<datum){i++}// 右指针左移while(arr[j]>datum){j--}// 交换if(i<j)swap(arr,i,j)// 当数组中存在重复数据时,即都为datum,但位置不同// 继续递增i,防止死循环if(arr[i]===arr[j]&&i!==j){i++}}returni}// 交换letswap=(arr,i,j)=>{lettemp=arr[i]arr[i]=arr[j]arr[j]=temp}
快速选择(quickselect)算法:快排会把所有的数据进行排序,而我们仅仅想要的是第 k 个最大值,所以 quickselect 对快排进行来优化:在每执行一次快排(partition)的时候,比较基准值位置是否在 n-k (第k大=第n-k小,n为数组长度)位置上,如果小于 n-k ,则第 k 个最大值在基准值的右边,我们只需递归快排基准值右边的子序列即可;如果大于 n-k ,则第 k 个最大值在基准值的做边,我们只需递归快排基准值左边的子序列即可;如果等于 n-k ,则第 k 个最大值就是基准值,平均时间复杂度O(n),最坏情况时间复杂度为O(n2),在最坏情况下,时间复杂度还是很高的
引言
今天这篇文章讲解一道活跃于各个大厂(腾讯、字节、阿里等)面试中的题目:Top K 问题,将按照以下脉络介绍:
下面直接开始吧👇
一、什么是 Top K 问题
经典的 Top K 问题有:最大(小) K 个数、前 K 个高频元素、第 K 个最大(小)元素
下面以求数组中的第 K 个最大元素为例,介绍 Top K 问题的解法
题目:
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素。
示例:
二、解法:全局排序,取第 k 个数
我们能想到的最简单的就是将数组进行排序(可以是最简单的快排),取前 K 个数就可以了,so easy
代码实现:
复杂度分析:
注意:
在 V8 引擎 7.0 版本之前,数组长度小于10时,
Array.prototype.sort()
使用的是插入排序,否则用快速排序。在 V8 引擎 7.0 版本之后就舍弃了快速排序,因为它不是稳定的排序算法,在最坏情况下,时间复杂度会降级到 O(n2)。
而是采用了一种混合排序的算法:TimSort 。
这种功能算法最初用于Python语言中,严格地说它不属于以上10种排序算法中的任何一种,属于一种混合排序算法:
在数据量小的子数组中使用插入排序,然后再使用归并排序将有序的子数组进行合并排序,时间复杂度为
O(nlogn)
。三、解法:局部排序,冒泡
题目仅仅需要求出数组中的第K个最大元素,没必要对数组整体进行排序
可以使用冒泡排序,每次将最大的数在最右边冒泡出来,只冒泡 k次即可
动图演示
代码实现
复杂度分析:
四、解法:构造前
k
个最大元素小顶堆,取堆顶我们也可以通过构造一个前
k
个最大元素小顶堆来解决,小顶堆上的任意节点值都必须小于等于其左右子节点值,即堆顶是最小值。所以我们可以从数组中取出
k
个元素构造一个小顶堆,然后将其余元素与小顶堆对比,如果大于堆顶则替换堆顶,然后堆化,所有元素遍历完成后,堆中的堆顶即为第k
个最大值具体步骤如下:
k
个数(0
到k-1
位),构造一个小顶堆k
位开始遍历数组,每一个数据都和小顶堆的堆顶元素进行比较,如果小于堆顶元素,则不做任何处理,继续遍历下一元素;如果大于堆顶元素,则将这个元素替换掉堆顶元素,然后再堆化成一个小顶堆。代码实现:
复杂度分析:
利用堆求 Top k 问题的优势
这种求 Top k 问题是可以使用排序来处理,但当我们需要在一个动态数组中求 Top k 元素怎么办喃?
动态数组可能会插入或删除元素,难道我们每次求 Top k 问题的时候都需要对数组进行重新排序吗?那每次的时间复杂度都为 O(nlogn)
这里就可以使用堆,我们可以维护一个 K 大小的小顶堆,当有数据被添加到数组中时,就将它与堆顶元素比较,如果比堆顶元素大,则将这个元素替换掉堆顶元素,然后再堆化成一个小顶堆;如果比堆顶元素小,则不做处理。这样,每次求 Top k 问题的时间复杂度仅为 O(logk)
更多堆内容可查看 前端进阶算法9:看完这篇,再也不怕堆排序、Top K、中位数问题面试了
五、解法:优化,快速选择(quickselect)算法
无论是排序算法还是构造堆求解 Top k问题,我们都经过的一定量的不必要操作:
k
的堆(大顶堆,小顶堆),同时花费时间也很昂贵,时间复杂度为O(nlogk)
那么有没有一种算法,不需要进行排序,也不需要花费额外的空间,就能获取第 K 个最大元素喃?
这就要说到快速选择算法了😊
快速选择(quickselect)算法与快排思路上相似,下面普及一下快排,已经了解的朋友可以跳过这一段
快排
快排使用了分治策略的思想,所谓分治,顾名思义,就是分而治之,将一个复杂的问题,分成两个或多个相似的子问题,在把子问题分成更小的子问题,直到更小的子问题可以简单求解,求解子问题,则原问题的解则为子问题解的合并。
快排的过程简单的说只有三步:
partition
)具体按以下步骤实现:
注意这里的基准该如何选择喃?最简单的一种做法是每次都是选择最左边的元素作为基准:
但这对几乎已经有序的序列来说,并不是最好的选择,它将会导致算法的最坏表现。还有一种做法,就是选择中间的数或通过
Math.random()
来随机选取一个数作为基准,下面的代码实现就是以随机数作为基准。代码实现
快排是从小到大排序,所以第
k
个最大值在n-k
位置上复杂度分析
2n)2n)快速选择(quickselect)算法
上面我们实现了快速排序来取第 k 个最大值,其实没必要那么麻烦,
我们仅仅需要在每执行一次快排的时候,比较基准值位置是否在
n-k
位置上,n-k
,则第 k 个最大值在基准值的右边,我们只需递归快排基准值右边的子序列即可;n-k
,则第 k 个最大值在基准值的做边,我们只需递归快排基准值左边的子序列即可;n-k
,则第 k 个最大值就是基准值代码实现:
复杂度分析:
六、解法:继续优化,中位数的中位数(BFPRT)算法
又称为中位数的中位数算法,它的最坏时间复杂度为 O(n) ,它是由Blum、Floyd、Pratt、Rivest、Tarjan提出。该算法的思想是修改快速选择算法的主元选取方法,提高算法在最坏情况下的时间复杂度。
在BFPTR算法中,仅仅是改变了快速选择(quickselect)算法中
Partion
中的基准值的选取,在快速选择(quickselect)算法中,我们可以选择第一个元素或者最后一个元素作为基准元,优化的可以选择随机一个元素作为基准元,而在 BFPTR 算法中,每次选择五分中位数的中位数作为基准元(也称为主元pivot),这样做的目的就是使得划分比较合理,从而避免了最坏情况的发生。BFPRT 算法步骤如下:
n/5
个组,每组 5 个元素,若有剩余,舍去n/5
个组中的每一组使用插入排序找到它们各自的中位数代码实现:
复杂度分析:
为什么是5?
在BFPRT算法中,为什么是选5个作为分组?
首先,偶数排除,因为对于奇数来说,中位数更容易计算。
如果选用3,有 ,其操作元素个数还是
n
。如果选取7,9或者更大,在插入排序时耗时增加,常数
c
会很大,有些得不偿失。七、回顾与总结
最后,我们总结一下,求topk问题其实并不难,主要有以下几个思路:
这里简单说一下后两种方案:
首先,需要知道什么是快排,快排的过程简单的说只有三步:首先从序列中选取一个数作为基准数;将比这个数大的数全部放到它的右边,把小于或者等于它的数全部放到它的左边 (一次快排
partition
);然后分别对基准的左右两边重复以上的操作,直到数组完全排序快速选择(quickselect)算法:快排会把所有的数据进行排序,而我们仅仅想要的是第 k 个最大值,所以
quickselect
对快排进行来优化:在每执行一次快排(partition
)的时候,比较基准值位置是否在n-k
(第k大=第n-k小,n为数组长度)位置上,如果小于n-k
,则第 k 个最大值在基准值的右边,我们只需递归快排基准值右边的子序列即可;如果大于n-k
,则第 k 个最大值在基准值的做边,我们只需递归快排基准值左边的子序列即可;如果等于n-k
,则第 k 个最大值就是基准值,平均时间复杂度O(n),最坏情况时间复杂度为O(n2),在最坏情况下,时间复杂度还是很高的中位数的中位数(bfprt)算法:该算法的思想是修改快速选择(
quickselect
)算法的主元选取方法,提高算法在最坏情况下的时间复杂度最后,附上Top K问题经典练习题,解题内容太多,前往git仓库查看解答
腾讯&字节等:最小的k个数:
腾讯&字节等:最小的k个数 #59
leetcode347:前 K 个高频元素:
leetcode347:前 K 个高频元素 #61
字节&leetcode215:数组中的第K个最大元素:
字节&leetcode215:数组中的第K个最大元素 #62
The text was updated successfully, but these errors were encountered: