1、100亿个数字找出最大的10个


1、首先一点,对于海量数据处理,思路基本上是确定的,必须分块处理,然后再合并起来。

2、对于每一块必须找出10个最大的数,因为第一块中10个最大数中的最小的,可能比第二块中10最大数中的最大的还要大。

3、分块处理,再合并。也就是Google MapReduce 的基本思想。Google有很多的服务器,每个服务器又有很多的CPU,因此,100亿个数分成100块,每个服务器处理一块,1亿个数分成100块,每个CPU处理一块。然后再从下往上合并。注意:分块的时候,要保证块与块之间独立,没有依赖关系,否则不能完全并行处理,线程之间要互斥。另外一点,分块处理过程中,不要有副作用,也就是不要修改原数据,否则下次计算结果就不一样了。

4、上面讲了,对于海量数据,使用多个服务器,多个CPU可以并行,显著提高效率。对于单个服务器,单个CPU有没有意义呢?

  也有很大的意义。如果不分块,相当于对100亿个数字遍历,作比较。这中间存在大量的没有必要的比较。可以举个例子说明,全校高一有100个班,我想找出全校前10名的同学,很傻的办法就是,把高一100个班的同学成绩都取出来,作比较,这个比较数据量太大了。应该很容易想到,班里的第11名,不可能是全校的前10名。也就是说,不是班里的前10名,就不可能是全校的前10名。因此,只需要把每个班里的前10取出来,作比较就行了,这样比较的数据量就大大地减少了。

2、寻找热门查询

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


必备知识:
    什么是哈希表?
    哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

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


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

第一步:Query统计
    Query统计有以下俩个方法,可供选择:
    1、直接排序法
    首先我们最先想到的的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数了。

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

    让我们回忆一下数据结构课程上的内容,当数据量比较大而且内存无法装下的时候,我们可以采用外排序的方法来进行排序,这里我们可以采用归并排序,因为归并排序有一个比较好的时间复杂度O(NlgN)。

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

    综合分析一下,排序的时间复杂度是O(NlgN),而遍历的时间复杂度是O(N),因此该算法的总体时间复杂度就是O(N+NlgN)=O(NlgN)。

    2、Hash Table法
    在第1个方法中,我们采用了排序的办法来统计每个Query出现的次数,时间复杂度是NlgN,那么能不能有更好的方法来存储,而时间复杂度更低呢?

    题目中说明了,虽然有一千万个Query,但是由于重复度比较高,因此事实上只有300万的Query,每个Query255Byte,因此我们可以考虑把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里,Hash Table绝对是我们优先的选择,因为Hash Table的查询速度非常的快,几乎是O(1)的时间复杂度。

    那么,我们的算法就有了:维护一个Key为Query字串,Value为该Query出现次数的HashTable,每次读取一个Query,如果该字串不在Table中,那么加入该字串,并且将Value值设为1;如果该字串在Table中,那么将该字串的计数加一即可。最终我们在O(N)的时间复杂度内完成了对该海量数据的处理。

    本方法相比算法1:在时间复杂度上提高了一个数量级,为O(N),但不仅仅是时间复杂度上的优化,该方法只需要IO数据文件一次,而算法1的IO次数较多的,因此该算法2比算法1在工程上有更好的可操作性。

3、海量日志数据,提取出某日访问百度次数最多的那个IP

首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有个2^32个IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。



或者如下阐述(雪域之鹰):
算法思想:分而治之+Hash



1、IP地址最多有2^32=4G种取值情况,所以不能完全加载到内存中处理;



2、可以考虑采用分而治之的思想,按照IP地址的Hash(IP) % 1024值,把海量IP日志分别存储到1024个小文件中,这样,每个小文件最多包含4MB个IP地址;



     这里解释一下为什么用Hash(IP) % 1024值,如果不用,而直接分类的话,可能会出现这样一种情况,就是有个IP在每个小文件中都存在,而且这个IP并不一定在那个小文件中是数量最多的,那么最终可能选择的结果会有问题,所以这里用了Hash(IP)%1024值,这样的话,通过计算IP的Hash值,相同IP肯定会放到一个文件中,当然了不同的IP的Hash值也可能相同,就存在一个小文件中。



3、对于每一个小文件,可以构建一个IP为key,出现的次数为value的Hash Map,同时记录当前出现次数最多的那个IP地址;



4、可以得到1024个小文件中的出现次数最多的那个IP,再依据常规的排序算法得出总体上出现次数最多的IP。



4、有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。

思路:首先要求得每个词的频率,1G无法放入内存,需要分成多个小文件,对每个小文件的词进行统计

(1)顺序读取文件,对每个词,可以hash(x)P00(只要不小于1024个文件,是为了保证每个小文件可以放入内存), 这样被映射为5000个小文件,每个文件大概200K,每个文件最少1250个单词

(2)对于每个小文件,利用hash_map记录每个单词出现的频率,选出频率最大的100个单词(可以用100个元素的最小堆),再生成对应的5000个文件分别包含这100个单词和频率,这分的文件太多了(关于分多少文件有什么准则吗?, 100*16*5000字节 > 1M, 无法放入内存),

(3)对这5000个小文件进行归并排序,选出最大的100个。

       以上也可以将从5000个文件得到的100个数,最后放入100个小文件吧,最后使用100路归并。

 

最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法的时间复杂度一般为O(nlogn),如快速排序。但是在32位的机器上,每个float类型占4个字节,1亿个浮点数就要占用400MB的存储空间,对于一些可用内存小于400M的计算机而言,很显然是不能一次将全部数据读入内存进行排序的。其实即使内存能够满足要求(我机器内存都是8GB),该方法也并不高效,因为题目的目的是寻找出最大的10000个数即可,而排序却是将所有的元素都排序了,做了很多的无用功。

        第二种方法为局部淘汰法,该方法与排序方法类似,用一个容器保存前10000个数,然后将剩余的所有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的10000个数还小,那么容器内这个10000个数就是最大10000个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这1亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为O(n+m^2),其中m为容器的大小,即10000。

        第三种方法是分治法,将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个数。

        第五种方法采用最小堆。首先读入前10000个数来创建大小为10000的最小堆,建堆的时间复杂度为O(mlogm)(m为数组的大小即为10000),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至1亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有10000个数字。该算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)。

5、有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。

(1)hash映射这10个文件到另外的10个文件中(hash(query)),这是为了让相同的query放入一个文件中

(2)对每个文件进行hash统计,统计出每个单词的频率,然后按照频率进行排序,使用快速/堆/归并都可以。将每个文件的结果,包含query和频率输出到10个文件中。

(3)对这10个文件进行归并排序。

       令因为重复查询比较多,对于所有的查询可以同时放入内存,这样可以将分成的10个文件一次装入内存,进行排序。

6、 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?

    思路:每个文件的大小5G*64 = 32G,远远大于内存,需要对a,b分别分成小文件

(1)利用一个hash(url)00,分别将a,b文件分别映射成1000个小文件,因为通过相同的映射函数,所以对于a,b,相同的url都在对应的文件中,(a0 vs b0, a1 vs b1等等)

(2)分别比对这1000个对应的小文件,可以通过先将a映射到一个hash表中,然后依次遍历b,判断是否在a中出现,出现过则说明重复

7、在2.5亿个整数中找出不重复的整数,注,内存不足以容纳这2.5亿个整数。

思路1:总共大小2.5*10^8*4字节=1G

(1)将这么多整数先hash(val)00分成1000个小文件,相同的数就在相同的文件中

(2)对每个小文件进行hash映射,统计出现次数,然后将对应次数为1的输出。

思路2:采用2-Bitmap(每个数分配2bit, 00表示不存在,01表示出现多次,11无意义),所有的整数总共需内存2^32次方,2^32 * 2 bit = 1G内存,如果可以存入内存,首先全部置为0, 依次遍历这2.5忆个整数,如果bitmap中是00则变01,01变10, 10保持不变,把01对应的数输出即可。

8、腾讯面试题:给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?     

思路1:同样采用位图,40忆个不重复的数,每个数用1bit表示,出现或不出现,40*10^8*1 = 0.5G大小。遍历这40忆个数,如果出现将对应位置为1,对于给定的数直接判断位图中对应的值。

思路2:编程珠玑上的一个思路。将每个整数都看成32位的二进制数,从最高位,依次按位来分,按最高位0,1分成两个文件,每个文件数字个数小于20亿,与所要判断的数的最高为进行比较,从而知道去哪个文件继续比较,然后对于选定的文件再按照次高位比较再分成2个文件,再比较判断数对应的位数,依次循环,直到最后一位,就可以找到或判断没有该数了。时间复杂度O(log2n),因为每次都将数据减少一半,直到最后一个。

9、怎么在海量数据中找出重复次数最多的一个?

思路:hash分成小文件,分别统计每个小文件数据出现次数,找出出现次数最大的,然后在将每个小文件的最大值进行比较,找到最大值,与上面思路一样的。

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

思路1:最小堆,找最大100个数

思路2:快速排序,每次分割之后只考虑比轴大的一部分(快速选择的思想),直到比轴大的一部分比100多时,采用传统排序,取前100个

思路3:选取前100个元素,排序,然后扫描剩余的元素,与排好序的元素中最小的相比,如果比它大,替换,重排前面,这跟堆排序思路一样。

11、5亿个整数找他们的中位数

思路:遍历这个文件,按照数的大小分别放入不同的文件,例如2^16个区域,每个区域大概9000个数的范围,然后统计每个区域的个数,然后就可以计算中位数落入哪个区域,并且可以计算出中位数是这个区域的第几大数,然后求出这个区域的第k个数就可以了。

分布式:

海量数据分布在100台电脑中,想个办法高效统计出这批数据的TOP10。
一共有N个机器,每个机器上有N个数。每个机器最多存O(N)个数并对它们操作。如何找到N^2个数的中数(median)?

       分析:思路其实是一样的,这只不过是分不到了n台电脑上,首先你不能保证一个元素只出现在一台电脑上,所以需要先通过hash映射,遍历所有的数据,对于第一道题,将所有相同的数据映射到同一台机器上,对于第2个问题,每个电脑上存放不同范围的数据,然后再进行统计,第1道题就可以用前面题的思路,对于找出每台机子的前10个数,然后再统计这些数,找到top10, 第2道题,统计每台机子数的个数,找出中位数所在机子,并计算出中位数是这个机子的第几个就找到了。 

 

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

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

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

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

(5)10亿个整数找出重复次数最多的100个整数。

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

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

 

重复问题
        在海量数据中查找出重复出现的元素或者去除重复出现的元素也是常考的问题。针对此类问题,一般可以通过位图法实现。例如,已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。

        本题最好的解决方法是通过使用位图法来实现。8位整数可以表示的最大十进制数值为99999999。如果每个数字对应于位图中一个bit位,那么存储8位整数大约需要99MB。因为1B=8bit,所以99Mbit折合成内存为99/8=12.375MB的内存,即可以只用12.375MB的内存表示所有的8位数电话号码的内容。

 

        总结:这些海量数据处理的题,思路基本差不多,首先是hash映射,成为不同类型的文件,然后hash统计,之后进行排序等等。以下是july总结的,以上也是参考其中博客整理一些思路的产物。。:

 

  1. 分而治之/hash映射 + hash统计 + 堆/快速/归并排序(频率最高,最大等);
  2. 双层桶划分(中位数, 不重复数):本质上还是分而治之的思想,重在“分”的技巧上!
    适用范围:第k大,中位数,不重复或重复的数字
    基本原理及要点:因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行
  3. Bloom filter/Bitmap(可以用来实现数据字典,进行数据的判重,或者集合求交集,不重复数);
  4. Trie树(数据量大,重复多,但是数据种类小可以放入内存)/数据库(适用范围:大数据量的增删改查)/倒排索引(适用范围:搜索引擎,关键字查询);
  5. 外排序:大数据排序,去重,外部排序的归并算法,置换选择败者树原理,最优归并数
  6. 分布式处理之Hadoop/Mapreduce:数据量大,但数据种类小可以放入内存,将数据交给不同的机器去处理,数据划分,结果规约