一文详解面试常考的TopK问题
首发公众号:
bigsai,转载请附上本文链接
前言
hello,大家好,我是bigsai哥哥,好久不见,甚是想念哇!
今天给大家分享一个TOPK问题,不过我这里不考虑特别大分布式的解决方案,普通的一道算法题。
首先搞清楚,什么是topK问题?
topK问题,就是找出序列中前k大(或小)的数,topK问题和第K大(或小)的解题思路其实大致一致的。
TopK问题是一个非常经典的问题,在笔试和面试中出现的频率都非常非常高(从不说假话)。下面,从小小白的出发点,认为topK是求前K大的问题,一起认识下TopK吧!
当前,在求TopK和第K大问题解法差不多,这里就用力扣215数组的第k个大元素 作为解答的题演示啦。学习topk之前,这篇程序员必知必会的十大排序一定要会。
排序法
找到TopK,并且排序TopK
啥,你想要我找到TopK?不光光TopK,你想要多少个,我给你多少个,并且还给你排序给排好,啥排序我最熟悉呢?
如果你想到冒泡排序O(n^2)那你就大意了啊。
如果使用O(n^2)级别的排序算法,那也是要优化的,其中冒泡排序和简单选择排序,每一趟都能顺序确定一个最大(最小)的值,所以不需要把所有的数据都排序出来,只需要执行K次就行啦,所以这种算法的时间复杂度也是O(nk)。
这里给大家回顾一下冒泡排序和简单选择排序区别:
冒泡排序和简单选择排序都是多趟,每趟都能确定一个最大或者最小,区别就是冒泡在枚举过程中只和自己后面比较,如果比后面大那么就交换;而简单选择是每次标记一个最大或者最小的数和位置,然后用这一趟的最后一个位置数和它交换(每一趟确定一个数枚举范围都慢慢变小)。
下面用一张图表示过程:

这里把code也给大家提供一下,简单选择上面图给的是每次选最小,实现的时候每次选最大就可以了。
//交换数组中两位置元素
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//冒泡排序实现
public int findKthLargest1(int[] nums, int k) {
for(int i=nums.length-1;i>=nums.length-k;i--)//这里也只是k次
{
for(int j=0;j<i;j++) {="" if(nums[j]="">nums[j+1])//和右侧邻居比较
{
swap(nums,j,j+1);
}
}
}
return nums[nums.length-k];
}
//简单选择实现
public int findKthLargest2(int[] nums, int k) {
for (int i = 0; i < k; i++) {//这里只需要K次
int max = i; // 最小位置
for (int j = i + 1; j < nums.length; j++) {
if (nums[j] > nums[max]) {
max = j; // 更换最小位置
}
}
if (max != i) {
swap(nums, i, max); // 与第i个位置进行交换
}
}
return nums[k-1];
}
当然,快排和归并排序甚至堆排序也可以啊,这些排序的时间复杂度为O(nlogn),也就是将所有数据排序完然后直接返回结果,这部分就不再详细讲解啦,调调api或者手写排序都可。
两种思路的话除了K极小的情况O(nk)快一些,大部分情况其实还是O(nlogn)情况快一些的,不过从O(n^2)想到O(nk),还是有所收获的。
基于堆排优化
这里需要知道堆相关的知识,我以前写过优先队列和堆排序,这里先不重复讲,大家也可以看一下:
上面说道堆排序O(nlogn)那是将所有元素都排序完然后取前k个,但是其实上我们分析一下这个堆排序的过程和几个注意点哈:
堆这种数据结构,分为大根堆和小根堆,小根堆是父节点值小于子节点值,大根堆是父节点的值大于子节点的值,这里肯定是要采用大根堆的。
堆看起来是一个树形结构,但是堆是个完全二叉树我们用数组存储效率非常高,并且也非常容易利用下标直接找到父子节点,所以都用数组来实现堆,每次排序完成的节点都将数移到数组末尾让一个新数组组成一个新的堆继续。
堆排序从大的来看可以分成两个部分,无序数组建堆和在堆基础上每次取对顶排序。其中无序数组建堆的时间复杂度为O(n),在堆基础上排序每次取堆顶元素,然后将最后一个元素移到堆顶进行调整堆,每次只需要O(logn)级别的时间复杂度,完整排序完n次就是O(nlogn),但是咱们每次只需要k次,所以完成k个元素排序功能需要花费O(klogn)时间复杂度,整个时间复杂度为O(n+klogn)因为和前面区分一下就不合并了。
画了一张图帮助大家理解,进行两次就获得Top2,进行k次就获得TopK了。

实现代码为:
class Solution {
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//下移交换 把当前节点有效变换成一个堆(大根)
public void shiftDown(int arr[],int index,int len)//0 号位置不用
{
int leftchild=index*2+1;//左孩子
int rightchild=index*2+2;//右孩子
if(leftchild>=len)
return;
else if(rightchild<len&&arr[rightchild]>arr[index]&&arr[rightchild]>arr[leftchild])//右孩子在范围内并且应该交换
{
swap(arr, index, rightchild);//交换节点值
shiftDown(arr, rightchild, len);//可能会对孩子节点的堆有影响,向下重构
}
else if(arr[leftchild]>arr[index])//交换左孩子
{
swap(arr, index, leftchild);
shiftDown(arr, leftchild, len);
}
}
//将数组创建成堆
public void creatHeap(int arr[])
{
for(int i=arr.length/2;i>=0;i--)
{
shiftDown(arr, i,arr.length);
}
}
public int findKthLargest(int nums[],int k)
{
//step1建堆
creatHeap(nums);
//step2 进行k次取值建堆,每次取堆顶元素放到末尾
for(int i=0;i<k;i++) {="" int="" team="nums[0];" nums[0]="nums[nums.length-1-i];//删除堆顶元素,将末尾元素放到堆顶" nums[nums.length-1-i]="team;" shiftdown(nums,="" 0,="" nums.length-i-1);="" 将这个堆调整为合法的大根堆,注意(逻辑上的)长度有变化="" }="" return="" nums[nums.length-k];="" ```="" ###="" 基于快排优化="" 上面堆排序都能优化,那么快排呢?="" 快排当然能啊,这么牛的事情怎么能少得了我快排呢?="" 这部分需要堆快排有一定了解和认识,前面很久前写过:[图解手撕冒泡和快排](https:="" mp.weixin.qq.com="" s="" 0tuu5h3604pq-ipnqd_d-w)="" (后面待优化),快排的核心思想就是:**分治**="" ,每次确定一个数字的位置,然后将数字分成两个部分,左侧比它小,右侧比它大,然后递归调用这个过程。每次调整的时间复杂度为o(n),平均次数为logn次,所以平均时间复杂度为o(nlogn)。="" ="" 但是这个和求topk有什么关系呢?="" 我们求topk,其实就是求比目标数字大的k个,我们随机选一个数字例如上面的5,5的左侧有4个,右侧有4个,可能会出现下面几种情况了:="" ①="" 如果k-1等于5右侧数量,那么说明中间这个5就是第k个,它和它的右侧都是topk。="" **②**如果k-1小于5**右侧数的数量**="" ,那么说明topk全在5的右侧,那么可以直接压缩空间成右侧继续递归调用同样方法查找。="" **③**="" 如果k-1大于5右侧的数量,那么说明右侧和5全部在topk中,然后左侧还有(**k-包括5右侧数总**数),此时搜查范围压缩,k也压缩。举个例子,如果**k="7**" 那么5和5右侧已经占了5个数字一定在top7中,我们只需要在5左侧找到top2就行啦。="" 这样一来每次数值都会被压缩,这里因为快排不是完全递归,时间复杂度不是o(nlogn)而是o(n)级别(详细的可以找一些网上证明),但是测试样例有些极端代码比如给你跟你有序1="" 2="" 3="" 4="" 5="" 6……="" 找top1="" 就出现比较极端的情况。所以具体时候会用一个随机数和第一个交换一下防止特殊样例(仅仅为了刷题用的),当然我这里为了就不加随机交换的啦,并且如果这里要得到的topk是未排序的。="" 详细逻辑可以看下实现代码为:="" ```java="" class="" solution="" public="" findkthlargest(int[]="" nums,="" k)="" quicksort(nums,0,nums.length-1,k);="" private="" void="" quicksort(int[]="" nums,int="" start,int="" end,int="" if(start="">end)
return;
int left=start;
int right=end;
int number=nums[start];
while (left<right){ while="" (number<="nums[right]&&left<right){" right--;="" }="" nums[left]="nums[right];" (number="">=nums[left]&&left<right){ left++;="" }="" nums[right]="nums[left];" nums[left]="number;" int="" num="end-left+1;" if(num="=k)//找到k就终止" return;="">k){
quickSort(nums,left+1,end,k);
}else {
quickSort(nums,start,left-1,k-num);
}
}
}
计数排序番外篇
排序总有一些骚操作的排序—线性排序,那么你可能会问桶类排序可以嘛?
也可以啦,不过要看数值范围进行优化,桶类排序适合数据均匀密集出现次数比较多的情况,而计数排序更是希望数值能够小一点。
那么利用桶类排序的具体核心思想是怎么样的呢?
先用计数排序统计各个数字出现次数,然后将新开一个数组从后往前叠加求和计算。

这种情况非常适合数值巨量并且分布范围不大的情况。
代码本来不想写了,但是念在你会给我三连我写一下吧
//力扣215
//1 <= k <= nums.length <= 104
//-104 <= nums[i] <= 104
public int findKthLargest(int nums[],int k)
{
int arr[]=new int[20001];
int sum[]=new int[20001];
for(int num:nums){
arr[num+10000]++;
}
for(int i=20000-1;i>=0;i--){
sum[i]+=sum[i+1]+arr[i];
if(sum[i]>=k)
return i-10000;
}
return 0;
}
结语
好啦,今天的TopK问题就到这里啦,相信你下次遇到肯定会拿捏它。
TopK问题不难,就是巧妙利用排序而已。排序是非常重要的,面试会非常高频。
这里我就不藏着掖着摊牌了,以面试官的角度会怎么引导你说TOPK问题。
狡猾的面试官:
嗯,我们来聊聊数据结构与算法,来讲讲排序吧,你应该接触过吧?讲出你最熟悉的三种排序方式,并讲解一下其中具体算法方式。
卑微的我:
bia la bia la bia la bia la……
如果你提到快排,桶排序说不定就让你用这个排序实现一下TopK问题,其他排序也可能,所以掌握好十大排序是非常必要的!
个人原创公众号:bigsai 欢迎关注,花了半年写了一本原创数据结构与算法pdf。

一文详解面试常考的TopK问题的更多相关文章
- 前端面试常考知识点---CSS
前端面试常考知识点---js 1.CSS3的新特性有哪些 点我查看 CSS3选择器 . CSS3边框与圆角 CSS3圆角border-radius:属性值由两个参数值构成: value1 / valu ...
- PHP面试常考之会话控制
你好,是我琉忆,欢迎您来到PHP面试专栏.本周(2019.2-25至3-1)的一三五更新的文章如下: 周一:PHP面试常考之会话控制周三:PHP面试常考之网络协议周五:PHP面试常考题之会话控制和网络 ...
- 一文详解Hexo+Github小白建站
作者:玩世不恭的Coder时间:2020-03-08说明:本文为原创文章,未经允许不可转载,转载前请联系作者 一文详解Hexo+Github小白建站 前言 GitHub是一个面向开源及私有软件项目的托 ...
- 一文详解 Linux 系统常用监控工一文详解 Linux 系统常用监控工具(top,htop,iotop,iftop)具(top,htop,iotop,iftop)
一文详解 Linux 系统常用监控工具(top,htop,iotop,iftop) 概 述 本文主要记录一下 Linux 系统上一些常用的系统监控工具,非常好用.正所谓磨刀不误砍柴工,花点时间 ...
- PHP面试常考内容之Memcache和Redis(2)
你好,是我琉忆.继周一(2019.2-18)发布的"PHP面试常考内容之Memcache和Redis(1)"后,这是第二篇,感谢你的支持和阅读.本周(2019.2-18至2-22) ...
- PHP面试常考内容之Memcache和Redis(1)
你好,是我琉忆.继上周(2019.2-11至2-15)发布的"PHP面试常考内容之面向对象"专题后,发布的第二个专题,感谢你的阅读.本周(2019.2-18至2-22)的文章内容点 ...
- PHP面试常考内容之面向对象(3)
PHP面试专栏正式起更,每周一.三.五更新,提供最好最优质的PHP面试内容.继上一篇"PHP面试常考内容之面向对象(2)"发表后,今天更新面向对象的最后一篇(3).需要(1),(2 ...
- PHP面试常考内容之面向对象(2)
PHP面试专栏正式起更,每周一.三.五更新,提供最好最优质的PHP面试内容.继上一篇"PHP面试常考内容之面向对象(1)"发表后,今天更新(2),需要(1)的可以直接点击文字进行跳 ...
- PHP面试常考内容之面向对象(1)
PHP中面向对象常考的知识点有以下几点,我将会从以下几点进行详细介绍说明,帮助你更好的应对PHP面试常考的面向对象相关的知识点和考题. 整个面向对象文章的结构涉及的内容模块有: 一.面向对象与面向过程 ...
随机推荐
- python 函数的定义及调用语法,map 方法,函数嵌套递归
1.什么是函数 开发程序时候,需要代码执行多次,为了提高编写效率及代码重用性,所以把具有独立功能的代码块组织为一个小模块,给这个功能一个名称,这就是函数. 函数可以使用系统自带的函数也可以 ...
- [bzoj4003]城市攻占
倍增,对于每一个点计算他走到$2^i$次祖先所需要的攻击力以及最终会变成什么(一个一次函数),简单处理即可(然而这样是错的,因为他只保证了骑士的攻击力可以存,并没有保证这个一次函数的系数可以存)(其实 ...
- 【JavaSE】Java基础·疑难点汇集
Java基础·疑难点 2019-08-03 19:51:39 by冲冲 1. 部分Java关键字 instanceof:用来测试一个对象是否是指定类型的实例. native:用来声明一个方法是由与 ...
- IDEA远程快速部署SpringBoot项目到Docker环境
一:基础准备 1.首先在linux服务器安装Docker环境,具体安装步骤及Docker使用参考官网或网络资料(这里重点是快速部署项目到Docker环境) 2.配置Docker远程连接端口 1.vim ...
- 洛谷 P4931 - [MtOI2018]情侣?给我烧了!(加强版)(组合数学)
洛谷题面传送门 A 了这道题+发这篇题解,就当过了这个七夕节吧 奇怪的过节方式又增加了 首先看到此题第一眼我们可以想到二项式反演,不过这个 \(T\) 组数据加上 \(5\times 10^6\) 的 ...
- Atcoder Regular Contest 058 D - 文字列大好きいろはちゃん / Iroha Loves Strings(单调栈+Z 函数)
洛谷题面传送门 & Atcoder 题面传送门 神仙题. mol 一发现场(bushi)独立切掉此题的 ycx %%%%%%% 首先咱们可以想到一个非常 naive 的 DP,\(dp_{i, ...
- Oracle——创建多个实例(数据库)、切换实例、登录数据库实例
oracle中怎么创建多个实例? 其实很简单,怎么创建第一个实例,其他实例应该也怎么创建. 我的理解其实在linux中的oracle数据库中创建一个实例,实际上就是创建一个新的数据库,只是实例名字不同 ...
- IDEA+maven+javafx(java 1.8)入坑记录
序 好久没写博客了,主要是因为懒,写博客真的是个难坚持的事.但今天登上来看了看,之前记录ctf写的wp竟然点击量这么多了,突然让我有了继续写下去的动力. 这段时间遇到了好多事,中间也有想过写几篇文章记 ...
- 生产调优4 HDFS-集群扩容及缩容(含服务器间数据均衡)
目录 HDFS-集群扩容及缩容 添加白名单 配置白名单的步骤 二次配置白名单 增加新服务器 需求 环境准备 服役新节点具体步骤 问题1 服务器间数据均衡 问题2 105是怎么关联到集群的 服务器间数据 ...
- add more
# -*- coding: utf-8 -*- print('123', 123) print(type('123'), type(123)) # string, integer /ˈintidʒə/ ...