数据结构面试题1.2.6-Top K算法详细解析——百度面试题

来源:互联网 发布:java程序员工作内容 编辑:程序博客网 时间:2024/05/16 14:04
资料:

十一、从头到尾解析Hash 表算法 http://blog.csdn.net/v_JULY_v/article/details/6256463
其实可以直接用hash-map的方法来算 http://www.360doc.com/content/11/0531/13/6890766_120702007.shtml
采用trie树 http://www.cnblogs.com/cherish_yimi/archive/2009/10/12/1581666.html
作者:July、wuliming、pkuoliver  

问题描述:百度面试题
搜索引擎会通过日志文件把用户每次检索使用的检索串都记录下来,每个查询串的长度为1-255字节。
假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是1千万,但如果除去重复后,不超过3百万个。一个查询串的重复数越高,说明查询它的用户越多,也就是越热门。),请你统计最热门的10个查询串,要求使用的内存不超过1个G。

必备知识:
什么是哈希表?
哈希表(hash table,也叫散列表),是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希表hash table(key,value)的做法很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
而当使用哈希表进行查询的时候,就是再次使用哈希函数将Key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位(文章第二、三部分,会对Hash表详细阐述)。

问题解析:
要统计最热门查询,首先就是要统计每个Query出现的次数,然后根据统计结果,找出Top 10。所以我们可以基于这个思路分两步来设计该算法。
即,此问题的解决分为以下两个步骤:

第一步:Query统计

Query统计有一下两个方法,可供选择:

1.直接排序法

首先我们最先想到的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数。
但是题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是255Byte,很显然要占据2.375G内存,这个条件就不满足要求了。
让我们回忆一下数据结构课程上的内容,当数据量比较大而且内存无法装下的时候,我们可以采用外排序的方法来进行排序,这里我们可以采用归并排序,因为归并排序有一个比较好的时间复杂度O(NlgN)。
排完序之后我们再对已经有序的Query文件进行遍历,统计每个Query出现的次数,再次写入文件中。
综合分析一下,排序的时间复杂度是O(NlgN),而遍历的时间复杂度为O(N),因此该算法的总体时间复杂度就是O(N+NlgN)=O(NlgN)。

2.Hash Table法

在第1个方法中,我们采用了排序的办法来统计每个Query出现的次数,时间复杂度是NlgN,那么能不能有更好的方法来存储,而时间复杂度更低呢?
题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query 255Byte,因此我们可以考虑把它们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常快,几乎是O(1)的时间复杂度。
那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的Hash Table,每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。
本算法相比算法1:在时间复杂度上提高了一个数量级,为O(N),但不仅仅是在时间复杂度上的优化,该方法只需要IO数据文件一次,而算法1的IO次数较多,因此该算法2比算法1在工程上有更好的操作性。

第二步:找出Top 10

算法1:普通排序

我想对于排序算法大家已经不陌生了,这里不再赘述,我们要注意的是排序算法的时间复杂度是NlgN,在本题中,三百万条记录,用1G内存是可以存下的。

算法2:部分排序

题目要求是求出Top 10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10个Query,按照每个Query的统计次数由大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰,加入当前Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top 10了。
不难分析,这样,算法的最坏时间复杂度是N*K,其中K是指Top多少。

算法3:堆

在算法二中,我们已经将时间复杂度由NlogN优化到N*K,不得不说这是一个比较大的改进了,可是有没有更好的算法呢?
分析一下,在算法2中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,一次我们每次查找的时候可以采用二分的方法查找,这样操作的的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法2有了改进。
基于以上分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?回答是肯定的,那就是堆。
借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此到这里,我们的算法可以改进为这样,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比
具体过程是,堆顶存放上的是整个堆中最小的数,现在遍历N个数,把先遍历到的K个数放到最小堆中,并假设他们就是我们要找到最大的K个数,X1>X2...Xmin(堆顶),而后遍历后续的N-K个数,一一与堆顶元素进行比较,如果遍历到的Xi大于堆顶元素Xmin,则把Xi放入堆中,而后更新整个堆,更新的时间复杂度为logK,如果Xi<Xmin,则不更新,整个过程的复杂度为O(K)+O((N - K) * logK) = O(N*logK)。
(堆排序的3D动画演示可以参看此链接:http://www.benfrederickson.com/2013/10/10/heap-visualization.html)
思想与上述算法2一致,只是在算法3中,我们采用了最小堆这种数据结构代替数组,把查找目标元素的时间复杂度由O(K)降到了O(logK)。
那么这样,采用堆数据结构,算法3最终的时间复杂度就降到了N’*logK,和算法2相比,又有了比较大的改进。
总结:
至此,算法就完全结束了,经过上述第一步:先用Hash表统计每个Query出现的次数,O(N);然后第二步:采用堆数据结构找出Top 10,N*O(logK)。所以,我们最终的时间复杂度是:O(N)+N‘*O(logK)。(N为1000万,N'为为3000万)。

0 0
原创粉丝点击