原文可见:Link,这里是我的总结版。
转载请注明出处
这里假设你对以下知识比较熟悉了:堆排序(Link), 散列(Link)
首先来看一道非常经典的题:有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
这道题其实解答是散列处理 + 统计频数 + 频数排序 + 结果融合的步骤。我们来一点点地讲
先来看一道简单一点的:
搜索引擎会通过日志文件把用户每次检索使用的所有检索串都记录下来, 每个查询串的 长度为 1-255 字节。 假设目前有一千万个记录(这些查询串的重复度比较高,虽然总数是 1 千万,但如果除 去重复后,不超过 3 百万个。一个查询串的重复度越高,说明查询它的用户越多,也就是越 热门。,请你统计最热门的 10 个查询串,要求使用的内存不能超过 1G。
这道题可以分为两步:统计频数+排序
问题解析: 要统计最热门查询,首先就是要统计每个 Query 出现的次数,然后根据统计结果,找出 Top 10。
第一步:统计每个query出现的次数:
最简单的统计频数的方法是:先排序,然户统计频数。
既然要排序,又是无法装下内存的时候,用外排序(外排序看这里)
这里就用归并排序(归并排序看这里),它的复杂度是O(NlgN),然后就是遍历统计频数了。这里的复杂度是O(N)
第二种:其实统计频数最有效的方法还是Hash table
这里说了去重复后,不超过 3 百万个,那么Hash存储key需要用到255Byte*300万,可以全装进去
而Hash Table 的查 询速度非常的快,几乎是 O(1)的时间复杂度。 那么,我们的算法就有了:维护一个 Key 为 Query 字串,Value 为该 Query 出现次数的 HashTable,每次读取一个 Query,如果该字串不在 Table 中,那么加入该字串,并且将 Value 值设为 1;如果该字串在 Table 中,那么将该字串的计数加一即可。最终我们在 O(N)的时间 复杂度内完成了对该海量数据的处理。
第二步,对query的频数排序:
由于这里去重复之后只有300万,不算海量,所以只需要堆排序就ok了
一种既能快速查找,又能快速移动元素的数据结构 呢?回答是肯定的,那就是堆()。 借助堆结构,我们可以在 log 量级的时间内查找和调整/移动。因此到这里,我们的算 法可以改进为这样,维护一个 K(该题目中是 10)大小的小根堆,然后遍历 300 万的 Query, 分别和根元素进行对比。
总结:
排序处理: N*O(logK)
堆排序:在每台电脑上求出TOP10,可以采用包含10个元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆)。比如求TOP10大,我们首先取前10个元素调整成最小堆,如果发现,然后扫描后面的数据,并与堆顶元素比较,如果比堆顶元素大,那么用该元素替换堆顶,然后再调整为最小堆。最后堆中的元素就是TOP10大。
统计频数: O(N)
Hash Table (当去重后可以装入内存)
================================================================================
海量数据上的散列分布处理 + 结果融合:
用个hash(url/word/IP)将大序列分割成小序列:
hash表将key value转化(通过固定的算法,即hash函数)成为一个整形数字,然后通过整形数字%表长的余数来决定这个key在表中的位置
在小序列上做操作--然后再整合(Map-Reduce思想)
例题:
1. 给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
可以估计每个文件安的大小为50G×64=320G,远远大于内存限制的4G。所以不可能将其完全加载到内存中处理。考虑采取分而治之的方法。
散列分布: 遍历文件a,对每个url求取,然后根据所取得的值将url分别存储到1000个小文件(记为)中。这样每个小文件的大约为300M。遍历文件b,采取和a相同的方式将url分别存储到1000各小文件(记为)。这样处理后,所有可能相同的url都在对应的小文件()中,不对应的小文件不可能有相同的url。然后我们只要求出1000对小文件中相同的url即可。
分布处理:求每对小文件中相同的url时,可以把其中一个小文件的url存储到hash_set中。然后遍历另一个小文件的每个url,看其是否在刚才构建的hash_set中,如果是,那么就是共同的url,存到文件里面就可以了。
结果融合:所有文件加起来
==================================================================================
例题:散列处理 + 统计频数 + 频数排序 + 结果融合
2. 有10个文件,每个文件1G,每个文件的每一行存放的都是用户的query,每个文件的query都可能重复。要求你按照query的频度排序。
散列处理: 顺序读取10个文件,按照hash(query)%10的结果将query写入到另外10个文件(记为)中。这样新生成的文件每个的大小大约也1G(假设hash函数是随机的)。
统计频数 + 频数排序(分布处理): 找一台内存在2G左右的机器,依次对用hash_map(query, query_count)来统计每个query出现的次数。利用快速/堆/归并排序按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件(记为)。
结果融合: 对这10个文件进行归并排序(内排序与外排序相结合)。???
3. 有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
散列分布:顺序读文件中,对于每个词x,取,然后按照该值存到5000个小文件(记为)中。这样每个文件大概是200k左右。如果其中的有的文件超过了1M大小,还可以按照类似的方法继续往下分,知道分解得到的小文件的大小都不超过1M。
分布处理:统计频数 + 频数排序 : 统计每个文件中出现的词以及相应的频率(可以采用trie树/hash_map等),并取出出现频率最大的100个词(可以用含100个结点的最小堆),并把100词及相应的频率存入文件,这样又得到了5000个文件。
结果融合:把这5000个文件进行归并(类似与归并排序)的过程了。
4. 海量日志数据,提取出某日访问百度次数最多的那个IP。
方案1:首先是这一天,并且是访问百度的日志中的IP取出来,逐个写入到一个大文件中。注意到IP是32位的,最多有个IP。同样可以采用映射的方法,比如模1000,把整个大文件映射为1000个小文件,再找出每个小文中出现频率最大的IP(可以采用hash_map进行频率统计,然后再找出频率最大的几个)及相应的频率。然后再在这1000个最大的IP中,找出那个频率最大的IP,即为所求。
相似例题:
6. 海量数据分布在100台电脑中,想个办法高校统计出这批数据的TOP10。
7. 怎么在海量数据中找出重复次数最多的一个?
10. 一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,请给出思想,给出时间复杂度分析。
11. 一个文本文件,找出前10个经常出现的词,但这次文件比较长,说是上亿行或十亿行,总之无法一次读入内存,问最优解。
12. 100w个数中找出最大的100个数。
总结:散列处理 + 统计频数 + 频数排序 + 结果融合的相关问题
首先判断是否要进行散列处理:
所谓的是否能一次读入内存,实际上应该指去除重复后的数据量。如果去重后数据可以放入内存,我们可以为数据建立字典,比如通过 map,hashmap,trie,然后直接进行统计即可。当然在更新每条数据的出现次数的时候,我们可以利用一个堆来维护出现次数最多的前N个数据,当 然这样导致维护次数增加,不如完全统计后在求前N大效率高。
如果数据无法放入内存。可以采用分布式计算,基本上就是map-reduce过程,首先可以根据数据值或者把数据hash(md5)后的值, 将数据按照范围划分到不同的机子,最好可以让数据划分后可以一次读入内存,这样不同的机子负责处理各种的数值范围,实际上就是map。得到结果后,各个机 子只需拿出各自的出现次数最多的前N个数据,然后汇总,选出所有的数据中出现次数最多的前N个数据,这实际上就是reduce过程。
为什么这里要通过hash+除模来散列而不是直接分开呢?因为要保证让不同的机器处理一个数值范围。
实际上可能想直接将数据均分到不同的机子上进行处理,这样是无法得到正确的解的。因为一个数据可能被均分到不同的机子上,而另一个则可能完全聚 集到一个机子上,同时还可能存在具有相同数目的数据。比如我们要找出现次数最多的前100个,我们将1000万的数据分布到10台机器上,找到每台出现次 数最多的前 100个,归并之后这样不能保证找到真正的第100个,因为比如出现次数最多的第100个可能有1万个,但是它被分到了10台机子,这样在每台上只有1千 个,假设这些机子排名在1000个之前的那些都是单独分布在一台机子上的,比如有1001个,这样本来具有1万个的这个就会被淘汰,即使我们让每台机子选 出出现次数最多的1000个再归并,仍然会出错,因为可能存在大量个数为1001个的发生聚集。因此不能将数据随便均分到不同机子上,而是要根据hash 后的值将它们映射到不同的机子上处理,让不同的机器处理一个数值范围。
为什么这里不用外排序:
外排序的方法会消耗大量的IO,效率不会很高。而上面的分布式方法,也可以用于单机版本,也就是将总的数据根据值的范围,划分成多个不同的子文件,然后逐个处理。处理完毕之后再对这些单词的及其出现频率进行一个归并。实际上就可以利用一个外排序的归并过程。
排序操作的融合---归并操作:
归并操作的过程如下:(O(m+n)的复杂度)
不依赖散列归并的除重操作:(针对于有限大小的整数序列):利用bit-map
Key:所有的整数序列的长度是2^32;一亿大概是N的26-27次方之间。
在2.5亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数。
采用2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存内存,还可以接受。然后扫描这2.5亿个整数,查看Bitmap中相对应位,如果是00变01,01变10,10保持不变。所描完事后,查看bitmap,把对应位是01的整数输出即可。
给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
维护一个2^32内存的位图,扫描,出现了a就置map[a]为1,那么对于b,通过判断map[b]是否为1就知道了
===========================
我是一个对数据结构还掌握的很不够的学生,上述叙述如果有不正之处敬请指出,感激不尽