数据结构和算法TopK算法秦羽的思考

本文主要来看看方便用代码解决的问题。

用堆排来解决TopK的思路很直接。

前面已经说过,堆排利用的大(小)顶堆所有子节点元素都比父节点小(大)的性质来实现的,这里故技重施:既然一个大顶堆的顶是最大的元素,那我们要找最小的K个元素,是不是可以先建立一个包含K个元素的堆,然后遍历集合,如果集合的元素比堆顶元素小(说明它目前应该在K个最小之列),那就用该元素来替换堆顶元素,同时维护该堆的性质,那在遍历结束的时候,堆中包含的K个元素是不是就是我们要找的最小的K个元素?

实现:在堆排的基础上,稍作了修改,buildHeap和heapify函数都是一样的实现,不难理解。

速记口诀:最小的K个用最大堆,最大的K个用最小堆。

适用场景实现的过程中,我们先用前K个数建立了一个堆,然后遍历数组来维护这个堆。这种做法带来了三个好处:(1)不会改变数据的输入顺序(按顺序读的);(2)不会占用太多的内存空间(事实上,一次只读入一个数,内存只要求能容纳前K个数即可);(3)由于(2),决定了它特别适合处理海量数据。

这三点,也决定了它最优的适用场景。

用快排的思想来解TopK问题,必然要运用到”分治”。

与快排相比,两者唯一的不同是在对”分治”结果的使用上。我们知道,分治函数会返回一个position,在position左边的数都比第position个数小,在position右边的数都比第position大。我们不妨不断调用分治函数,直到它输出的position=K-1,此时position前面的K个数(0到K-1)就是要找的前K个数。

实现:“分治”还是原来的那个分治,关键是getTopK的逻辑,务必要结合注释理解透彻,自动动手写写。

适用场景对照着堆排的解法来看,partition函数会不断地交换元素的位置,所以它肯定会改变数据输入的顺序;既然要交换元素的位置,那么所有元素必须要读到内存空间中,所以它会占用比较大的空间,至少能容纳整个数组;数据越多,占用的空间必然越大,海量数据处理起来相对吃力。

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个。

一、排序

排序是最容易想到的方法,将n个数排序之后,取出最大的k个,即为所得。

伪代码:

sort(arr,1,n);

returnarr[1,k];

分析:明明只需要TopK,却将全局都排序了,这也是这个方法复杂度非常高的原因。那能不能不全局排序,而只局部排序呢?这就引出了第二个优化方法。

二、局部排序

不再全局排序,只对最大的k个排序。

冒泡是一个很常见的排序方法,每冒一个泡,找出最大值,冒k个泡,就得到TopK。

for(i=1tok){

bubble_find_max(arr,i);

}

分析:冒泡,将全局排序优化为了局部排序,非TopK的元素是不需要排序的,节省了计算资源。不少朋友会想到,需求是TopK,是不是这最大的k个元素也不需要排序呢?这就引出了第三个优化方法。

三、堆

思路:只找到TopK,不排序TopK。

先用前k个元素生成一个小顶堆,这个小顶堆用于存储,当前最大的k个元素。

接着,从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素,并调整堆,以保证堆内的k个元素,总是当前最大的k个元素。

直到,扫描完所有n-k个元素,最终堆中的k个元素,就是猥琐求的TopK。

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

for(i=k+1ton){

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

returnheap[k];

分析:堆,将冒泡的TopK排序优化为了TopK不排序,节省了计算资源。堆,是求TopK的经典算法,那还有没有更快的方案呢?

四、随机选择

这个方法并不是所有同学都知道,为了将算法讲透,先聊一些前序知识,一个所有程序员都应该烂熟于胸的经典算法:快速排序。

画外音:

(1)如果有朋友说,“不知道快速排序,也不妨碍我写业务代码呀”…额...

(2)除非校招,我在面试过程中从不问快速排序,默认所有工程师都知道;

其伪代码是:

voidquick_sort(int[]arr,intlow,inthigh){

if(low==high)return;

inti=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,是一个典型的运用减治法思想的算法,其伪代码是:

intBS(int[]arr,intlow,inthigh,inttarget){

if(low>high)return-1;

mid=(low+high)/2;

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

if(arr[mid]>target)

returnBS(arr,low,mid-1,target);

else

returnBS(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]划分成左右两个子数组:

以上述TopK的数组为例,先用第一个元素t=arr[low]为划分依据,扫描一遍数组,把数组分成了两个半区:

partition返回的是t最终的位置i。

画外音:把整个数组扫一遍,比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);

画外音:这一段非常重要,多读几遍。

这就是随机选择算法randomized_select,RS,其伪代码如下:

intRS(arr,low,high,k){

if(low==high)returnarr[low];

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

if(temp>=k)

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

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

再次强调一下:

通过随机选择(randomized_select),找到arr[1,n]中第k大的数,再进行一次partition,就能得到TopK的结果。

五、总结

TopK,不难;其思路优化过程,不简单:

应用场景:

必备知识:什么是哈希表?哈希表(Hashtable,也叫散列表),是根据关键码值(Keyvalue)而直接进行访问的数据结构。

也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

哈希表的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。问题解析:

即,此问题的解决分为以下俩个步骤:

第一步:Query统计(统计出每个Query出现的次数)Query统计有以下俩个方法,可供选择:1、直接排序法(经常在日志文件中统计时,使用catfile|formatkey|sort|uniq-c|sort-nr|head-n10,就是这种方法)首先我们最先想到的的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数了。

但是题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是255Byte,很显然要占据2.375G内存,这个条件就不满足要求了。

排完序之后我们再对已经有序的Query文件进行遍历,统计每个Query出现的次数,再次写入文件中。

那么,我们的算法就有了:

算法二:部分排序题目要求是求出Top10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10个Query,按照每个Query的统计次数由大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰(还是要放在合适的位置,保持有序),加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。

分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,一次我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法二有了改进。

基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?

总结:

/

问题一:

对数组进行降序全排序,然后返回前K个元素,即是需要的K个最大数。

算法思想2(比较好):

观察第一种算法,问题只需要找出一个数组里面前K个最大数,而第一种算法对数组进行全排序,不单单找出了前K个最大数,更找出了前N(N为数组大小)个最大数,显然该算法存在“冗余”,因此基于这样一个原因,提出了改进的算法二。

原文:

可以发现如果一次读入那么机器的内存肯定是受不了的,因此我们只有想其他方法解决,解决方式为了高效还是得符合一定的该概率解决,结果并不一定准确,但是应该可以作对大部分的数据。

算法思想1、1、我们可以把1亿个浮点数利用哈希分为了1000个组(将相同的数字哈希到同一个数组中);

2、第一次在每个组中找出最大的1W个数,共有1000个;

2、一个文本文件,找出前10个经常出现的词,但这次文件比较长,说是上亿行或十亿行,总之无法一次读入内存,问最优解。

方案1:首先根据用hash并求模,将文件分解为多个小文件,对于单个文件利用上题的方法求出每个文件件中10个最常出现的词。然后再进行归并处理,找出最终的10个最常出现的词。

3、100w个数中找出最大的100个数。

优化的方法:可以把所有10亿个数据分组存放,比如分别放在1000个文件中。这样处理就可以分别在每个文件的10^6个数据中找出最大的10000个数,合并到一起在再找出最终的结果。

以上就是面试时简单提到的内容,下面整理一下这方面的问题:

针对topK类问题,通常比较好的方案是分治+Trie树/hash+小顶堆(就是上面提到的最小堆),即先将数据集按照Hash方法分解成多个小数据集,然后使用Trie树活着Hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有topK中求出最终的topK。

第三种方法是分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100*10000个数据里面找出最大的10000个。如果100万数据选择足够理想,那么可以过滤掉1亿数据里面99%的数据。100万个数据里面查找最大的10000个数据的方法如下:用快速排序的方法,将数据分为2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大的那堆个数N大于10000个,继续对大堆快速排序一次分成2堆,如果大堆个数N小于10000个,就在小的那堆里面快速排序一次,找第10000-n大的数字;递归以上过程,就可以找到第1w大的数。参考上面的找出第1w大数字,就可以类似的方法找到前10000大数字了。此种方法需要每次的内存空间为10^6*4=4MB,一共需要101次这样的比较。

第四种方法是Hash法。如果这1亿个书里面有很多重复的数,先通过Hash法,把这1亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的10000个数。

下面针对不容的应用场景,分析了适合相应应用场景的解决方案。

如果需要查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9*8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。这种方法简单快速,使用。然后,也可以先用HashMap求出每个词出现的频率,然后求出频率最大的10个词。

这时可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同(1)类似,最后一个线程将结果归并。

该方法存在一个瓶颈会明显影响效率,即数据倾斜。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。而针对此问题,解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,知道所有数据处理完毕,最后由一个线程进行归并。

这种情况下,需要将原数据文件切割成一个一个小文件,如次啊用hash(x)%M,将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,知道每个小文件小于内存大小,这样每个文件可放到内存中处理。采用(1)的方法依次处理每个小文件。

这种情况,为了合理利用多台机器的资源,可将数据分发到多台机器上,每台机器采用(3)中的策略解决本地的数据。可采用hash+socket方法进行数据分发。

THE END
1.排列三2023144期艾薇儿三字诀(天齐原创)大乐透走势图 大乐透开奖结果 广东11选5走势图 排列三走势图 排列三开奖结果 排列三试机号 七星彩走势图 排列五走势图 排列五开奖结果 七星彩开奖结果 七乐彩走势图 七乐彩开奖结果 友情链接:彩吧图库 | 彩吧资讯 | 3d走势图 | 3d开机号近十期 | 开奖公告 | 天齐网 | 3D之家 | 彩经网 官方https://www.55128.cn/zt/pl3zm/5062442.htm
2.排列三2024312期[鱿鱼]三字诀原创排列三2024312期[鱿鱼]三字诀-原创旋风吹,马踏云;画弦素,声浅繁。发布时间:2024-11-20 22:53 工具:P3过滤缩水P3试机号走势图推荐:宝彩APP 免费方案 智能荐号 热点导航 公告: 天齐新版APP<中大多>上线,点击领鲜最新文章24321期一品公子P3天机诗 24321期王滨体彩p3字谜 24321期崂山道士P3字谜 24321期武当https://m.800820.net/p/9492292.html
3.十拿九稳1注排列3综合乐彩网第317期体彩P3十拿九稳1注推荐杀一码连中3 第317期体彩P3十拿九稳1注推荐 稳杀1码:3 稳推5码:04789 稳推4码:4789 稳推三胆:489 单选复式:02457*02478*02479 主推30注: 009,024,027,028,029,045,048,059,078,099,229,244,247,249,255,257,258,259,279,289,447,455,457,459,488,499,559https://p3.17500.cn/lotinx-m/thread-11207044.html
4.19分钟详解一分快?3规律口诀一分快?3规律口诀+Aゞ:1798925】網:RY99.CC】【域名手动在浏览器打开】【独家团队】【精准计划】【万人聊天室】不论昨日如何,今天都是全新的。勇敢地迈出第一步,因为你的梦想正等着你 推荐视频 已经到底了 热门视频 已经到底了 https://www.sohu.com/a/831311833_122132229
5.www.scmc国产又粗一又大一 8天前 台湾淫女吴梦梦 女人自慰app一区二区 2天前 手机在线观看AV 看看一级黄片A免费两个干一个 2天前 四川成人性爱免费视频 性老妇性一级性AA级 3天前 免费观看一级大黄片 黄色淫网站哪里有淫插插插一 9天前 97精品二? xxxxx69 4天前 饮尿の圣水在线观看 国产成人激情http://www.scmc-xa.com/xxxr761559
6.奇门遁甲术奇门遁甲起局排盘口诀详解飞盘奇门下面就根据口诀一步一步的详细讲解奇门遁甲排盘的步骤: 一、先把阳历时间转换为干支时间 1、庚:0,辛:1,壬:2,癸:3,甲:4,乙:5,丙:6,丁:7,戊:8,己:9,癸:0。 通过年份尾数我们可以快速得出年份的天干。比如今年是龙年2024年,今年年份尾数是4,对应的是甲,所以今年是甲辰年。 http://qimen.yi958.com/Article_10268.html
7.3个5相乘列乘法算式你还在背乘法口诀吗?这样以点带面,从若干口诀辐射到所以口诀,效果应该会比较明显。 学会乘法后, 孩子解题时还会出错? 会呀, 不熟悉肯定会的。 比如还有2个5相乘是多少?2个8相乘是多少?我家中招过。 速速用这个方式背口诀吧! 2个1相乘得1 2个2相乘得4 2个3相乘得9https://blog.csdn.net/weixin_39762856/article/details/110154699
8.学驾心得:科目一速记口诀,考试很轻松科目一考试必备口诀: 1分 不带证照乱用灯,门厢未关先启程, 会车倒车安全带,载人载物违规行。 2分 匝道超车超疲劳,接打手机无线到, 违规牵引故障车,客二货三还不到, 实习期内四类车,交叉路口人行道, 违反标志和标线,安全距离安全帽。 3分 五成以下超时速,牵引挂车违规定, https://m.jiakaobaodian.com/mip/news/detail/t-1-2aeuy4p.html
9.二年级下册数学《表内除法》教案12篇同学们带来了27个心形气球,每9个摆一行,可以摆几行? 同学们独立解决这个问题。(27÷9=3,除数是9,想9的口诀三九二十七,商是3. ) (4)小结,揭题 1、算法:除数是几,就想几的乘法口诀 2、用到了7、8、9的乘法口诀,板书课题用7、8、9的乘法口诀求商。 https://www.unjs.com/jiaoan/shuxue/20230313193805_6650845.html
10.长篇!41种单反摄影技巧,摄影入门必看的小窍门。吴越职业培训学校二十七、简单实用的用光口诀! 通俗好记的用光口诀,帮你摆脱用光烦恼! 基本控制: 1、加:背景明亮主体暗,使用闪光作补偿 2、减:主体明亮背景暗,找个东西挡住光 3、乘:反光板要巧使用,摆放位置要妥当 4、除:光束单一照脸上,塑料口袋派用场 具体控制: https://www.hnwuyue.com/fashion/meijiazixun/1509.html
11.键盘指法练习教学设计8篇(全文)具体操作如下:①步进式练习:按照指法的要求,由易到难,分层练习,如:先训练打基准位,再加入中指上下移动击键,接着再加入无名指,再加入小指……,然后补齐各排各键位进行练习,最后加入常用的功能键进行全键盘练习。在教学功能键的运用时,应重示范、多操作,举一反三,各个击破。②重复式练习:在步进式训练基础上,https://www.99xueshu.com/w/filekx49s4cx.html
12.步骤图糖醋排骨—1+2+3+4简单口诀做出南京大牌档的风味的做法【糖醋排骨—1+2+3+4简单口诀做出南京大牌档的风味】01.排骨冷水下锅焯水,等水开后,加入少许料酒,再煮4-5分钟,拂去血水。,11.排骨捞出,用热水冲洗干净,沥水的同时,拿一个小碗调汁,1+2+3+4,12根左右的排骨差不多是一勺料酒+两勺生抽+三勺冰糖+4勺醋,如果喜欢甜的,https://m.xiachufang.com/recipe/104319343/
13.文明礼仪教案五年级(通用8篇)文明礼仪教案五年级(通用8篇) 篇1:文明礼仪教案五年级 让文明礼仪之花盛开 我国是文明古国,礼仪之帮,讲道德、懂礼仪是学生健康成长的需要,是走向社会,进行交往的必备条件,但由于社会、学校、家庭等诸多因素,现在的小学生对“文明礼仪”的意识越来越来淡薄.在部分学生中出现了“不尊重教师”、“不尊重同学”、“不https://www.360wenmi.com/f/filewe6u99wl.html