一次搞透,面试中的TopK问题!

前言:本文将介绍随机选择,分治法,减治法的思想,以及TopK问题优化的来龙去脉,原理与细节,保证有收获。
 
面试中,TopK,是问得比较多的几个问题之一,到底有几种方法,这些方案里蕴含的优化思路究竟是怎么样的,今天和大家聊一聊。
画外音:除非校招,我在面试过程中从不问TopK这个问题。
 
问题描述
从arr[1, n]这n个数中,找出最大的k个数,这就是经典的TopK问题。
 
栗子
从arr[1, 12]={5,3,7,1,8,2,9,4,7,2,6,6} 这n=12个数中,找出最大的k=5个。
 
一、排序
一次搞透,面试中的TopK问题!
排序是最容易想到的方法,将n个数排序之后,取出最大的k个,即为所得。
 
伪代码

sort(arr, 1, n);

return arr[1, k];

 
时间复杂度:O(n*lg(n))

分析:明明只需要TopK,却将全局都排序了,这也是这个方法复杂度非常高的原因。那能不能不全局排序,而只局部排序呢?这就引出了第二个优化方法。
 
二、局部排序
不再全局排序,只对最大的k个排序。
一次搞透,面试中的TopK问题!
冒泡是一个很常见的排序方法,每冒一个泡,找出最大值,冒k个泡,就得到TopK
 
伪代码

for(i=1 to k){

         bubble_find_max(arr,i);

}

return arr[1, k];

 
时间复杂度:O(n*k)
 
分析:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。
 
三、堆
思路:只找到TopK,不排序TopK。
一次搞透,面试中的TopK问题!
先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素
 
一次搞透,面试中的TopK问题!
接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。
 
一次搞透,面试中的TopK问题!
直到,扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK。
 
伪代码

heap[k] = make_heap(arr[1, k]);

for(i=k+1 to n){

         adjust_heap(heep[k],arr[i]);

}

return heap[k];

 
时间复杂度:O(n*lg(k))
画外音:n个元素扫一遍,假设运气很差,每次都入堆调整,调整时间复杂度为堆的高度,即lg(k),故整体时间复杂度是n*lg(k)。
 
分析:堆,将冒泡的TopK排序优化为了TopK不排序,节省了计算资源。堆,是求TopK的经典算法那还有没有更快的方案呢?
 
四、随机选择
随机选择算在是《算法导论》中一个经典的算法,其时间复杂度为O(n),是一个线性复杂度的方法。
 
这个方法并不是所有同学都知道,为了将算法讲透,先聊一些前序知识,一个所有程序员都应该烂熟于胸的经典算法:快速排序
画外音:
(1)如果有朋友说,“不知道快速排序,也不妨碍我写业务代码呀”…额...
(2)除非校招,我在面试过程中从不问快速排序,默认所有工程师都知道;
 
其伪代码是

void quick_sort(int[]arr, int low, inthigh){

         if(low== high) return;

         int i = partition(arr, low, high);

         quick_sort(arr, low, i-1);

         quick_sort(arr, i+1, high);

}

 
其核心算法思想是,分治法
 
分治法(Divide&Conquer)把一个大的问题,转化为若干个子问题(Divide)每个子问题“”解决,大的问题便随之解决(Conquer)。这里的关键词是“都”。从伪代码里可以看到,快速排序递归时,先通过partition把数组分隔为两个部分,两个部分“都”要再次递归。
 
分治法有一个特例,叫减治法
 
减治法(Reduce&Conquer)把一个大的问题,转化为若干个子问题(Reduce)这些子问题中“”解决一个,大的问题便随之解决(Conquer)。这里的关键词是“只”
 
二分查找binary_search,BS,是一个典型的运用减治法思想的算法,其伪代码是:

int BS(int[]arr, int low, inthigh, int target){

         if(low> high) return -1;

         mid= (low+high)/2;

         if(arr[mid]== target) return mid;

         if(arr[mid]> target)

                   return BS(arr, low, mid-1, target);

         else

                   return BS(arr, mid+1, high, target);

}

 
从伪代码可以看到,二分查找,一个大的问题,可以用一个mid元素,分成左半区,右半区两个子问题。而左右两个子问题,只需要解决其中一个,递归一次,就能够解决二分查找全局的问题。
 
通过分治法与减治法的描述,可以发现,分治法的复杂度一般来说是大于减治法的
快速排序:O(n*lg(n))
二分查找:O(lg(n))
 
话题收回来,快速排序核心是:
i = partition(arr, low, high);
 
这个partition是干嘛的呢?
顾名思义,partition会把整体分为两个部分。
更具体的,会用数组arr中的一个元素(默认是第一个元素t=arr[low])为划分依据,将数据arr[low, high]划分成左右两个子数组:
(1)左半部分,都比t大;
(2)右半部分,都比t小;
(3)中间位置i是划分元素;
一次搞透,面试中的TopK问题!
以上述TopK的数组为例,先用第一个元素t=arr[low]为划分依据,扫描一遍数组,把数组分成了两个半区:
(1)左半区比t大;
(2)右半区比t小;
(3)中间是t;
partition返回的是t最终的位置i
 
很容易知道,partition的时间复杂度是O(n)。
画外音:把整个数组扫一遍,比t大的放左边,比t小的放右边,最后t放在中间N[i]。
 
partition和TopK问题有什么关系呢?
TopK是希望求出arr[1,n]中最大的k个数,那如果找到了第k大的数,做一次partition,不就一次性找到最大的k个数了么?
画外音:即partition后左半区的k个数。
 
问题变成了arr[1, n]中找到第k大的数
 
再回过头来看看第一次partition,划分之后:
i = partition(arr, 1, n);
(1)如果i大于k,则说明arr[i]左边的元素都大于k,于是只递归arr[1, i-1]里第k大的元素即可;
(2)如果i小于k,则说明说明第k大的元素在arr[i]的右边,于是只递归arr[i+1, n]里第k-i大的元素即可;
画外音:这一段非常重要,多读几遍。
 
这就是随机选择算法randomized_select,RS,其伪代码如下:

int RS(arr, low, high, k){

  if(low== high) return arr[low];

  i= partition(arr, low, high);

  t = i-low; //数组前半部分元素个数

  if(t>=k)

      return RS(arr, low, i-1, k); //求前半部分第k大

  else

      return RS(arr, i+1, high, k-t); //求后半部分第k-t大

}

 
一次搞透,面试中的TopK问题!
这是一个典型的减治算法,递归内的两个分支,最终只会执行一个,它的时间复杂度是O(n)。
 
再次强调一下:
(1)分治法,大问题分解为小问题,小问题都要递归各个分支,例如:快速排序;
(2)减治法,大问题分解为小问题,小问题只要递归一个分支,例如:二分查找,随机选择;
 
通过随机选择(randomized_select),找到arr[1, n]中第k大的数,再进行一次partition,就能得到TopK的结果。
 
五、总结
TopK,不难;其思路优化过程,不简单:
(1)全局排序,O(n*lg(n));
(2)局部排序,只排序TopK个数,O(n*k);
(3)堆,TopK个数也不排序了,O(n*lg(k));
(4)分治法,每个分支“都要”递归,例如:快速排序,O(n*lg(n));
(5)减治法,“只要”递归一个分支,例如:二分查找O(lg(n)),随机选择O(n);
(6)TopK的另一个解法:随机选择+partition;
 
知其然,知其所以然。
思路比结论重要
希望大家对TopK有新的认识,谢
架构师之路-分享可落地的架构文章
 
挖坑
TopK,还有没有更快的方法,且听下回分解。

发布者:糖太宗,转载请注明出处:https://www.qztxs.com/archives/science/technology/5983

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年5月11日 下午8:50
下一篇 2022年5月11日 下午8:52

相关推荐

  • AWVS扫描器IAST使用

    0x00 前言 很久没有更新blog了,这次把几个笔记分享一下,AWVS自带的IAST功能,很多人都不知道,这里记录一下IAST如何使用   0x01 扫描器部署 这里笔记写的较早,版本还是awvs13 1 2 3 4 5 6 7 8 9 10 # pull 拉取下载镜像 docker pull secfa/docker-awvs # 将Docke...

    2022年6月13日
    9100
  • MySQL,在线热备的内核原理!

    这是一篇关于MySQL数据库,redo log,LSN,崩溃恢复,在线热备的长文,耐心读完,如果没有收获,可以捶我。   研发的童鞋每次对MySQL库表做重大操作之前,例如: (1)修改表结构; (2)批量修改或者删除数据; 都会向DBA申请进行数据库的备份。 画外音:又或者说,不备份直接操作啦?   那DBA童鞋是怎么进行MySQL备份的呢?  ...

    2022年5月11日
    1500
  • 帖子中心,1亿数据,架构如何设计?

    帖子中心,是互联网业务中,一类典型的“1对多”业务,即:一个用户能发布多个帖子,一个帖子只有一个发布者。   随着数据量的逐步增大,并发量的逐步增大,帖子中心这种“1对多”业务,架构应该如何设计,有哪些因素需要考虑,是本文将要系统性讨论的问题。 什么是x对x? 所谓的“1对1”,“1对多”,“多对多”,来自数据库设计中的“实体-关系”ER模型,用来描述实体之...

    2022年5月14日
    2100
  • 如何排查死锁,死锁是怎么产生的,如何处理死锁

    之前刚学习多线程时,由于各种锁的操作不当,经常不经意间程序写了代码就发生了死锁,不是在灰度测试的时候被测出来,就是在代码review的时候被提前发现。 这种死锁的经历不知道大家有没有,不过怎么说都是一个面试高频题目,面试官是肯定希望你经历过的,没经历过那也得看看某八股文职业选手的文章装作经历过。 那么什么是死锁呢?为什么会产生死锁呢? 什么是死锁 敖丙和小美...

    2022年5月18日
    5600
  • 99%的人会答错,你敢不敢挑战一下?

    用户产品讲直觉,商业产品更讲逻辑。 设计师讲直觉,工程师更讲逻辑。 打德州,一部分同学讲究“就赌最后一张了”让人血脉喷张,一部分同学“没把牌计算概率”让人崩溃。 这样一些有意思的问题,凭直觉99%会答错。 问题一 一枚硬币,随机投掷一次,是正面和反面的概率各为50%。 随机投掷两次,有四种可能,正正,反反,正反,反正,概率各为25%。   现在,投掷第一次,...

    技术 2022年5月11日
    3200

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信