TopK问题

本文介绍了Top K问题,即在大量数据中找出出现频率或大小排名前K的元素。常见方法包括分治+Trie树/Hash+小顶堆、局部淘汰法、分治法、Hash法和最小堆法。根据不同的内存限制和场景,如单机单核、多核、受限内存和分布式环境,提出了不同的解决方案,强调了算法的扩展性和容错性在大规模数据处理中的重要性。并举例说明如何解决1G文件中频率最高的100个词、海量日志数据中IP的访问频率等实际问题。

海量数据中寻找TopK问题

Top K问题介绍

  所谓的Top K问题:在海量数据中找出出现频率最好的前K个数,或者从海量数据中找出最大的前K个数。例如,在搜索引擎中,统计搜索最热门的10个查询词/在歌曲库中统计下载最高的前10首歌等。针对Top K问题,通常方案是分治+Trie树/Hash+小顶堆,即先将数据集按照Hash方法分解成多个小数据集,然后使用Trie树/Hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后将每个数据集的Top K汇总起来并求出最终的Top K。
  Top K大(小顶堆)、Top K小(大顶堆)。这里讨论Top K大先拿10000个数建堆,然后一次添加剩余元素,如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆,这样,遍历完后,堆中的10000个数就是所需的最大的10000个。建堆时间复杂度应该是O(m)。堆调整的时间复杂度是O(logm) ,最终时间复杂度等于1次建堆时间+n次堆调整时间=O(m+nlogm)=O(nlogm)。进一步优化:可以将10亿个数据分组存放,如放在1000个文件中。分别处理每个文件的10^6个数据中找出最大的10000个数,合并到一起在再找出最终的结果。

  假如有1亿个浮点数,找出其中最大的10000个
  第一种方法为将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。
  第二种方法为局部淘汰法,该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。
  第三种方法是分治法,将1亿个数据分成100份,每份100万个数据,找到每份数据中最大的10000个,最后在剩下的100* 10000个数据里面找出最大的10000个。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个数。
  第五种方法采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。
  实际上,最优的解决方案应该是最符合实际设计需求的方案,在实际应用中,可能有足够大的内存,那么直接将数据扔到内存中一次性处理即可,也可能机器有多个核,这样可以采用多线程处理整个数据集。下面针对不容的应用场景,分析适合相应应用场景的解决方案:
【1、单机+单核+足够大内存】
  如果需要查找10亿个查询次(每个占8B)中出现频率最高的10个,考虑到每个查询词占8B,则10亿个查询次所需的内存大约是10^9 * 8B=8GB内存。如果有这么大内存,直接在内存中对查询次进行排序,顺序遍历找出10个出现频率最大的即可。也可以用HashMap求出每个词出现的频率,然后求出频率最大的10个词。
【2、单机+多核+足够大内存】
  可以直接在内存总使用Hash方法将数据划分成n个partition,每个partition交给一个线程处理,线程的处理逻辑同第一种类似,最后一个线程将结果归并。因为有分区操作那么就可能出现数据倾斜,从而影响效率。每个线程的处理速度可能不同,快的线程需要等待慢的线程,最终的处理速度取决于慢的线程。解决的方法是,将数据划分成c×n个partition(c>1),每个线程处理完当前partition后主动取下一个partition继续处理,直到所有数据处理完毕,最后由一个线程进行归并。
【3、单机+单核+受限内存】
  用Hash(x)%M将原数据文件切割成一个一个小文件,将原文件中的数据切割成M小文件,如果小文件仍大于内存大小,继续采用Hash的方法对数据文件进行分割,知道每个小文件小于内存大小,这样每个文件可放到内存中处理。采用单核的方法依次处理每个小文件。
【4、多机+受限内存】
  为了合理利用多台机器的资源,可将数据分发到多台机器上,每台机器采用第3种策略解决本地的数据。数据分发可采用hash+socket方法。
  从实际应用的角度考虑,(1)(2)(3)(4)方案并不可行,因为在大规模数据处理环境下,作业效率并不是首要考虑的问题,算法的扩展性和容错性才是首要考虑的。算法应该具有良好的扩展性,以便数据量进一步加大(随着业务的发展,数据量加大是必然的)时,在不修改算法框架的前提下,可达到近似的线性比;算法应该具有容错性,即当前某个文件处理失败后,能自动将其交给另外一个线程继续处理,而不是从头开始处理。因此采用【MapReduce】解决Top K问题。对于Map函数,采用Hash算法,将Hash值相同的数据交给同一个Reduce task;对于第一个Reduce函数,采用HashMap统计出每个词出现的频率,对于第二个Reduce 函数,统计所有Reduce task,输出数据中的Top K即可。与将数据均分到不同的机器上不同,因为一个数据可能被均分到不同的机器上,而另一个则可能完全聚集到一个机器上,同时还可能存在具有相同数目的数据。

重复问题
  在海量数据中查找出重复出现的元素或者去除重复出现的元素也是常考的问题。一般可以通过位图法实现,BitMap位图 或 Bloom Filter布隆过滤器 或 HashSet集合。每个元素对应一个bit处理。例如,已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。最好的解决方法是通过使用位图法来实现。8位整数可以表示的最大十进制数值为99999999。如果每个数字对应于位图中一个bit位,那么存储8位整数大约需要99MB。因为1B=8bit,所以99Mbit折合成内存为99/8=12.375MB的内存,即可以只用12.375MB的内存表示所有的8位数电话号码的内容。

排序问题
外排序 或 BitMap位图。分割文件+文件内排序+文件之间归并。

Top K类问题:
(1)有10000000个记录,这些查询串的重复度比较高,如果除去重复后,不超过3000000个。一个查询串的重复度越高,说明查询它的用户越多,也就是越热门。请统计最热门的10个查询串,要求使用的内存不能超过1GB。

(2)有10个文件,每个文件1GB,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。按照query的频度排序。

(3)有一个1GB大小的文件,里面的每一行是一个词,词的大小不超过16个字节,内存限制大小是1MB。返回频数最高的100个词。

(4)提取某日访问网站次数最多的那个IP。

(510亿个整数找出重复次数最多的100个整数。

(6)搜索的输入信息是一个字符串,统计300万条输入信息中最热门的前10条,每次输入的一个字符串为不超过255B,内存使用只有1GB。

(7)有1000万个身份证号以及他们对应的数据,身份证号可能重复,找出出现次数最多的身份证号。

Top K问题举例及解答

Top K问题
【1】 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M,返回频数最高的100个词
  ①分治:顺序读文件,对每个词x取Hash(x)%2000,按照该值存到2000个小文件中。每个文件是500k左右。如果有文件超过了1M则继续分割,时间复杂度O(N)
  ②Trie树/HashMap:字符串用Trie树最好。对每个小文件,统计其中出现的词频。O(N) * (平均字符长度),时间复杂度O(N)
  ③小顶堆:用容量为100的小顶堆,以频率为value值插入,取每个文件现频率最大的100个词,把这100个词及相应的频率存入文件。最差O(N)* lg(100),也就是O(N)。注:步骤2,3合起来需要一轮磁盘存取过程,则可以缩减存入文件的个数,因为主要开销在磁盘读取上。减少文件读取次数,可以在每个文件存取最大容量的字符数量,对于该题1*(M/16字节字符串长度+频率(int)8字节)的数存到一个文件中,如20000个词存在一个文件中,可以缩减到10个文件,这样最后一步只需要读取10次就可以了。
  ④归并:将得到的10个文件里面的数进行归并,取前100个词。不需要多路归并,因为是找Top100的数,归并排序首先是O(NlgN)的复杂度以及频繁的磁盘存取,这里最好是在内存建立容量为100的小顶堆,依次读文件,遍历每个文件中的元素更新小顶堆,只需10次存取,并且时间复杂度是O(Nlog100),即O(N)。

一些思考这道题对空间有限制,为何还能选择Trie树的以空间换时间
  1、字符串会通过一个Hash算法映射为一个正整数然后对应到Hash表中的一个位置,表中记录的value值是次数。Hash表中是不是不用存储字符串本身,因为如果不存储字符串本身,是比较省空间的。因为Tire树找到一个字符串也是要按位置比较一遍,所以效率差不多。但其实字符串的Hash是要存储字符串本身的,不管是开放地址法还是散列表法,都无法做到不冲突。除非桶个数是字符串的所有情况26^16,但是空间不允许,因此Hash表中key值必须存着字符串本身,那么Hash在空间上肯定是定比不过Trie树的,因为Trie树对公共前缀只存储一次。
  2、Trie树如何以空间换取时间?这句话其实是相对于二叉查找树而言的,之所以效率高,是因为二叉查找树每次查找都要比较大小,并且因为度为2,查找深度很大,比较次数也多,因此效率差。而Trie树是按位进行Hash的,比如26个字母组成的字符串,每次找对应位的字符-‘a’就是位置了。而且度是26,查找深度就是字符串位数,查找起来效率自然就很快。但是为啥说是空间换时间,是因为字符串的Trie树若想存储所有的可能字符串,比如16位,一个点要对应下一位26种情况,也就是26个分支,也得26^16个位置,所以空间是很大的。但是Trie树的话可以采用依次插入的,不需要每个点记录26个点,而是只存在有值的分支,Trie树节点只要存频率次数,插入的流程就是挨个位子找分支,没有就新建,有就次数+1就行了。因此空间上很省,因为重复前缀就统计一次,而效率很高,O(length)。

【2】海量日志数据,提取出某日访问百度次数最多的IP
  分析:思路与题1一致,不需要考虑trie树
  ①分治:IP是32位,共有232个IP。访问该日的日志,将IP取出来,采用Hash,比如模1000,把所有IP存入1000个小文件。
  ②HashMap:统计每个小文件中出现频率最大的IP,记录其频率。
  ③小顶堆:这里用一个变量即可。在这1000个小文件各自最大频率的IP中,然后直接找出频率最大的IP。

【3】海量数据分布在100台电脑中,高效统计出这批数据的TOP10
  分析:主要不同点在于分布式,虽然数据已经是分布的,但是如果直接求各自的Top10然后合并的话,可能忽略一种情况,即有一个数据在每台机器的频率都是第11,但是总数可能属于Top10。所以应该先把100台机器中相同的数据整合到相同的机器,然后再求各自的Top10并合并。
  ①分治:顺序读每台机器上的数据,按照Hash(x)%100重新分布到100台机器内。接下来变成了单机的Top K问题。单台机器内的文件如果太大,可以继续Hash分割成小文件。
  ②HashMap:统计每台机器上数据的频率。
  ③小顶堆:采用容量为10的小顶堆,统计每台机器上的Top10。然后把这100台机器上的TOP10组合起来,共1000个数据,再用小顶堆求出TOP10。

【4】一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,并给出时间复杂度分析
   分析:文件大小不需要分割文件
  ①分治:一万行不算多,不用分割文件。
  ②Trie树:统计每个词出现的次数,时间复杂度是O(N* le) (le表示单词的平均长度)。
  ③小顶堆:容量为10的小顶堆,找出词频最多的前10个词,时间复杂度是O(N* lg10) (lg10表示堆的高度)。
总的时间复杂度是 O(N* le)与O(N* lg10)中较大的那一个。

【5】一个文本文件,找出前10个经常出现的词,但这次文件比较长,有上亿行或十亿行,总之无法一次读入内存,问最优解
  分析:比题4多一次分割。分割成可以一次读入内存的大小。
  ①分治:顺序读文件,将文件Hash分割成小文件,求小文件里的词频。
  ②③同上。

【6】100w个数中找出最大的100个数
  方法1:用容量为100的小顶堆查找。复杂度为O(100w * lg100)。
  方法2:采用快速排序的思想,每次分割之后只考虑比标准值大的那一部分,直到大的部分在比100多且不能分割的时候,采用传统排序算法排序,取前100个。复杂度为O(1000000*100)。
  方法3:局部淘汰法。取前100个元素并排序,然后依次扫描剩余的元素,插入到排好序的序列中,并淘汰最小值。复杂度为O(1000000 * lg100) (lg100为二分查找的复杂度)。

重复问题
【1】给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
  分析:每个文件的大小约为5G×64=320G,远远大于内存大小。考虑采取分而治之的方法。
方法1:
  ①分治:遍历文件a,对每个url求Hash%1000,根据值将url分别存储到1000个小文件中,每个小文件约为300M。文件b采用同样hash策略分到1000个小文件中。上述两组小文件中,只有相同编号的小文件才可能有相同元素。
  ②Hash_set:读取a组中一个小文件的url存储到hash_set中,然后遍历b组中相同编号小文件的每个url,查看是否在刚才构建的hash_set中。如果存在,则存到输出文件里。

方法2:
  如果允许有一定的错误率,可以使用Bloom filter,使用位数组,4G内存大概可以表示340亿bit。将其中一个文件中的url使用Bloom filter映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否在Bloom filter中。如果是,那么该url应该是共同的url(注意会有一定的错误率)。
  注: bloom filter被用来检测一个元素是不是集合中的一个成员。如果检测结果为是,该元素不一定在集合中;但如果检测结果为否,该元素一定不在集合中。主要思路是:将一个元素映射到一个 m 长度的阵列上,使用 k 个哈希 函数对应 k 个点,如果所有点都是 1 的话,那么元素在集合内,如果有 0 的话,元素则不在集合内。 错误率:如何根据输入元素个数n,确定位数组m的大小及hash函数个数k,k=(ln2)*(m/n)时错误率最小,为f = (1 – e-kn/m)k 。

【2】在2.5亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数
  分析:2.5亿个整数大概是954MB,也不是很大。当然可以更节省内存。整数一共2^32个数.每个数用2bit的话,需要1GB。也就是
方法1:
  采用2-Bitmap,每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义。共需内存60MB左右。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完后,查看Bitmap,把对应位是01的整数输出。
  注:感觉这个方法不对呀,bitmap要统计所有的整数值,2*3^32是需要1GB内存呀,不是60MB, 954MB都存不下怎么存1GB?? 得到结论,bitmap统计整数存在性起码得有1G的内存。也就是说少于268435456个数不如直接hash,消耗的内存反而更小!

方案2:
  分治法,Hash分割成小文件处理。注意hash保证了每个文件中的元素一定不会在其他文件中存在。利用Hash_set,在小文件中找出不重复的整数,再进行归并。

方案3:
  或者,我觉得可以将整个整数域划的bitmap根据内存大小分成可以几个文件,比如划分四个文件,这样的话0-1230在一个范围,,……,3*230-42^30在一个文件中,内存只要保证250M大小即可。整数需要放在对应的bitmap里面的对应位置,这里位置使用的是相对偏移量(value-首元素大小)。跟方案2相比分割的 方法不一样,以及每个小文件可以使用bitmap方法,所以更快一些。只是不知道有没有这种分割。

【3】一个文件包含40亿个整数,找出不包含的一个整数。分别用1GB内存和10MB内存处理
1GB内存:
  ①Bitmap:对于32位的整数,共有2^32个,每个数对应一个bit,共需0.5GB内存。遍历文件,将每个数对应的bit位置1。最后查找0bit位即可。

10MB内存: 10MB约为8 × 10^bit左右
  ①分治:将所有整数分段,每1M个数对应一个小文件,共4000个小文件。注意计算机能表示的所有整数有4G个。
  ②HashSet:对每个小文件,遍历并加入HashSet,最后如果set的size小于1M,则有不存在的数。利用Bitmap查找该数。
  注:考虑到磁盘IO次数越少越好,计算机能表示的整数个数一共有4G个,整数域Hash分割成10M一个文件,一共分割成400个小文件,每个小文件判断不存在的数,再把这些数全都归并起来。

【4】有10亿个URL,每个URL对应一个非常大的网页,怎样检测重复的网页?
  分析:不同的URL可能对应相同的网页,所以要对网页求Hash。1G个URL+哈希值,总量为几十G,单机内存无法处理。
  ①分治:根据Hash%1000,将URL和网页的哈希值分割到1000个小文件中,注意:重复的网页必定在同一个小文件中。
  ②HashSet:顺序读取每个文件,将Hash值加入集合,如果已存在则为重复网页。

排序问题
【1】有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求按照query的频度排序
方案1:
  ①分治:顺序读10个文件,按照Hash(query)%10的结果将query写入到另外10个文件。新生成的每个文件大小为1G左右(假设hash函数是随机的)。
  ②HashMap:找一台内存为2G左右的机器,用HashMap(query, query_count)来统计次数。
  ③内排序:利用快速/堆/归并排序,按照次数进行排序。将排序好的query和对应的query_count输出到文件中,得到10个排好序的文件。
  ④多路归并:将这10个文件进行归并排序。

方案2:
  一般query的总量是有限的,只是重复的次数比较多。对于所有的query,一次性就可能加入到内存。这样就可以采用Trie树/Hash_map等直接统计每个query出现的次数,然后按次数做快速/堆/归并排序就可以了

方案3:
  与方案1类似,在做完Hash分割后,将多个文件采用分布式的架构来处理(比如MapReduce),最后再进行合并。

【2】一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到这N^2个数的中位数?
方法1: 32位的整数一共有2^32个
  ①分治:把0到2^ 32-1的整数划分成N段,每段包含2^32/N个整数。扫描每个机器上的N个数,把属于第一段的数放到第一个机器上,属于第二段的数放到第二个机器上,依此类推。 (如果有数据扎堆的现象,导致数据规模并未缩小,则继续分割)
  ②找中位数的机器:依次统计每个机器上数的个数并累加,直到找到第k个机器,加上其次数则累加值大于或等于N^ 2/2,不加则累加值小于N^2/2。
  ③找中位数:设累加值为x,那么中位数排在第k台机器所有数中第N^ 2/2-x位。对这台机器的数排序,并找出第N^ 2/2-x个数,即为所求的中位数。复杂度是O(N^2)。

方法2:
  ①内排序:先对每台机器上的数进行排序。
  ②多路归并:将这N台机器上的数归并起来得到最终的排序。找到第N^ 2/2个数即是中位数。复杂度是O(N^2*lgN)。

【3】10G个整数,乱序排列,要求找出中位数,内存限制为2G
首先假设是32位无符号整数

  1. 读一遍10G个整数,把整数映射到256M个区段中,用一个64位无符号整数给每个相应区段记数。
    说明:整数范围是0 - 2^ 32 - 1,一共有4G种取值,映射到256M个区段,则每个区段有16(4G/256M = 16)种值,每16个值算一段, 0~15是第1段,16~31是第2段,……2^ 32-16 ~2^32-1是第256M段。一个64位无符号整数最大值是0~8G-1,这里先不考虑溢出的情况。总共占用内存256M×8B=2GB
  2. 从前到后对每一段的计数累加,当累加的和超过5G时停止,找出这个区段(即累加停止时达到的区段,也是中位数所在的区段)的数值范围,设为[a,a+15],同时记录累加到前一个区段的总数,设为m。然后,释放除这个区段占用的内存
  3. 再读一遍10G个整数,把在[a,a+15]内的每个值计数,即有16个计数
  4. 对新的计数依次累加,每次的和设为n,当m+n的值超过5G时停止,此时的这个计数所对应的数就是中位数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值