秒杀掉99%的海量数据处理面试题
一、基础知识:
- bit:位
- byte:字节
- 1 byte= 8 bit
- int 类型为 4 byte,共32位bit,unsigned int也是
- 2^32 byte = 4G
- 1G= 2^30 =10.7亿
二、海量数据处理概述:
1. 什么是海量数据?
所谓海量数据处理,就是指数据量太大,无法在较短时间内迅速解决,或者无法一次性装入内存。而解决方案就是:针对时间,可以采用巧妙的算法搭配合适的数据结构,如 Bloom filter/Hashmap/bit-map/堆/数据库/倒排索引/Trie树;针对空间,大而化小,分而治之(hash映射),把规模大化为规模小的,各个击破。
所以,海量数据处理的基本方法总结起来分为以下几种:
- 分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
- Trie树/Bloom filter/Bitmap
- 数据库/倒排索引;
- 双层桶划分;
- 外排序;
- 分布式处理之Hadoop/Mapreduce
2. 海量数据方法总结?
分析这些问题,不外乎查找和统计。看题目具体要求有没有限制内存大小和时间复杂度,然后套用下述的几种方法(秒杀掉:99%的海量数据处理面试题):
2.1 Bloom Filter
- 适用范围:可以用来实现数据字典,进行数据的判重,或者集合求交集
- 基本原理及要点:对于原理来说很简单,位数组+k个独立hash函数。将hash函数对应的值的位数组置1,查找时如果发现所有hash函数对应位都是1说明存在,很明显这个过程并不保证查找的结果是100%正确的。同时也不支持删除一个已经插入的关键字,因为该关键字对应的位会牵动到其他的关键字。所以一个简单的改进就是 counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。
- 扩展:Bloom filter将集合中的元素映射到位数组中,用k(k为哈希函数个数)个映射位是否全1表示元素在不在这个集合中。Counting bloom filter(CBF)将位数组中的每一位扩展为一个counter,从而支持了元素的删除操作。Spectral Bloom Filter(SBF)将其与集合元素的出现次数关联。SBF采用counter中的最小值来近似表示元素的出现频率。
- 使用工具:Bloom Filter;hash函数
2.2 Hash
适用范围:快速查找,删除的基本数据结构,通常需要总数据量可以放入内存
基本原理及要点:
- hash函数选择,针对字符串,整数,排列,具体相应的hash方法。
- 碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。
使用工具:hash函数(hash表);堆;
这种方法是典型的“分而治之”的策略,也是解决空间限制最常用的方法。基本思路可以用下图表示。先借助哈希算法,计算每一条数据的hash值,按照hash值将海量数据分布存储到多个桶中(所谓桶,一般可以用小文件实现)。根据hash函数的唯一性,相同的数据一定在同一个桶中。如此,我们再依次处理这些小文件,最后做合并运算即可(有点类似于Map-Reduce的思想)。
2.3 Bit-Map
- 适用范围:可进行数据的快速查找,判重,删除,一般来说数据范围是int的10倍以下
- 基本原理及要点:使用bit数组来表示某些元素是否存在,比如8位电话号码
- 扩展:bloom filter可以看做是对bit-map的扩展
2.4 堆(Heap)
- 适用范围:海量数据前n大,并且n比较小,堆可以放入内存
- 基本原理及要点:最大堆求前n小,最小堆求前n大。方法,比如求前n小,我们比较当前元素与最大堆里的最大元素,如果它小于最大元素,则应该替换那个最大元素。这样最后得到的n个元素就是最小的n个。适合大数据量,求前n小,n的大小比较小的情况,这样可以扫描一遍即可得到所有的前n元素,效率很高。
- 扩展:双堆,一个最大堆与一个最小堆结合,可以用来维护中位数。
2.5 双层桶划分
- 本质上就是【分而治之】的思想,重在“分”的技巧上!
- 适用范围:第k大,中位数,不重复或重复的数字
- 基本原理及要点:因为元素范围很大,不能利用直接寻址表,所以通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。
- 多层桶结构其实和最开始我们用hash映射分割大文件的思路是一致的,都是一种“分而治之”的策略。只不过多层桶结构是对有些桶分割之后再分割,构成了一种层次化的结构,它主要应用于一次分割的结果依然不能解决内存限制的情况。
2.6 数据库索引
- 适用范围:大数据量的增删改查
- 基本原理及要点:利用数据的设计实现方法,对海量数据的增删改查进行处理。
2.7 倒排索引(Inverted Index)
- 适用范围:搜索引擎,关键字查询
- 基本原理及要点:为何叫倒排索引?一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。
2.8 外排序
- 适用范围:大数据的排序,去重
- 基本原理及要点:外排序的归并方法,置换选择败者树原理,最优归并树
- 外排序是经典的排序方法之一,主要是针对大数据不能直接读入内存的情况,通过内存与外存之间的数据交换,实现海量数据排序的。
2.9 Trie树
- 适用范围:数据量大,重复多,但是数据种类小可以放入内存
- 基本原理及要点:实现方式,节点孩子的表示方式
- 使用工具:Trie树;堆
- Trie树是一种非常强大的处理海量字符串数据的工具。尤其是大量的字符串数据中存在前缀时,Trie树特别好用。Trie树在字典的存储,字符串的查找,求取海量字符串的公共前缀,以及字符串统计等方面发挥着重要的作用。用于存储时,Trie树因为不重复存储公共前缀,节省了大量的存储空间;用于以字符串的查找时,Trie树依靠其特殊的性质,实现了在任意数据量的字符串集合中都能以O(len)的时间复杂度完成查找(len为要检索的字符串长度);在字符串统计中,Trie树能够快速记录每个字符串出现的次数。
2.10 MapReduce
- 适用范围:数据量大,但是数据种类小可以放入内存
- 基本原理及要点:将数据交给不同的机器去处理,数据划分,结果归约。
MapReduce是一种计算模型,分为”Map”和”Reduce”两个阶段。
- Map:将大量数据分割后(或者将困难任务分解后)“各个击破”;
- Reduce:将“各个击破”的结果按照一定的规律进行合并操作;
这样做的好处是可以在任务被分解后,可以通过大量机器进行并行计算,从而突破时间或者空间的限制。时间上显然更快,空间上,单个机器只需要处理空间允许的数据量即可。举个简单的例子就是归并排序,我们先将数据集分割成小数据集,使用多个机器排序每个分割后的小数据集(Map),再将处理好的归并段依次归并(Reduce)。
3. 为什么这么难以处理?
海量数据处理的困难用一句话概括,就是时空资源不够。具体来说,
- 时间受限:无法在有限时间内,完成针对海量数据的某项处理工作;
- 空间受限:无法将海量数据一次性读入内存
对于时间受限的问题,我们一般的解决办法是高效的算法配合恰当的数据结构,比如哈希表,Bloom Filter,堆,倒排索引,tire树);而对于空间受限的问题,一般的解决办法是“大而化小,分而治之”的策略,既然一次性行不通,那就一部分一部分读,每读入一部分可以生成一个小文件,小文件是可以直接读入内存的,我们这样分割大数据之后,再依次处理小文件的数据。
总结:
- 1. 数据量过大,数据中什么情况都可能存在。
- 2. 软硬件要求高,系统资源占用率高。
- 3. 要求很高的处理方法和技巧。
三、详细讲解
1.分而治之/hash映射 + hashmap统计 + 快速/归并/堆排序
这种方法是典型的“分而治之”的策略,是解决空间限制最常用的方法,即海量数据不能一次性读入内存,而我们需要对海量数据进行的计数、排序等操作。基本思路如下图所示:先借助哈希算法,计算每一条数据的 hash 值,按照 hash 值将海量数据分布存储到多个桶中。根据 hash 函数的唯一性,相同的数据一定在同一个桶中。如此,我们再依次处理这些小文件,最后做合并运算即可
问题1:海量日志数据,统计出某日访问百度次数最多的那个IP
解决方式:IP地址最多有 2^32 = 4G 种取值情况,所以不能完全加载到内存中进行处理,采用 hash分解+ 分而治之 + 归并 方式:
(1)按照 IP 地址的 Hash(IP)%1024 值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
(2)对于每一个小文件,构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址
(3)然后再在这1024组最大的IP中,找出那个频率最大的IP
问题2:有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
解决思想: hash分解+ 分而治之 + 归并
(1)顺序读文件中,对于每个词x,按照 hash(x)/(1024*4) 存到4096个小文件中。这样每个文件大概是250k左右。如果其中的有的文件超过了1M大小,还可以按照hash继续往下分,直到分解得到的小文件的大小都不超过1M。
(2)对每个小文件,可以采用 trie树/hashmap 统计每个文件中出现的词以及相应的频率,并使用 100个节点的小顶堆取出出现频率最大的100个词,并把100个词及相应的频率存入文件。这样又得到了4096个文件。
(3)下一步就是把这4096个文件进行归并的过程了
问题3:有a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
解决方案1: hash 分解+ 分而治之 + 归并 的方式:
如果内存中想要存入所有的 url,共需要 50亿 * 64= 320G大小空间,所以采用 hash 分解+ 分而治之 + 归并 的方式:
(1)遍历文件a,对每个 url 根据某种hash规则,求取hash(url)/1024,然后根据所取得的值将 url 分别存储到1024个小文件(a0~a1023)中。这样每个小文件的大约为300M。如果hash结果很集中使得某个文件ai过大,可以在对ai进行二级hash(ai0~ai1024),这样 url 就被hash到 1024 个不同级别的文件中。
(2)分别比较文件,a0 VS b0,…… ,a1023 VS b1023,求每对小文件中相同的url时:把其中一个小文件的 url 存储到 hashmap 中,然后遍历另一个小文件的每个url,看其是否在刚才构建的 hashmap 中,如果是,那么就是共同的url,存到文件中。
(3)把1024个文件中的相同 url 合并起来
解决方案2:Bloom filter
如果允许有一定的错误率,可以使用 Bloom filter,4G内存大概可以表示 340 亿bit,n = 50亿,如果按照出错率0.01算需要的大概是650亿个bit,现在可用的是340亿,相差并不多,这样可能会使出错率上升些,将其中一个文件中的 url 使用 Bloom filter 映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)
问题4:有10个文件,每个文件1G,每个文件的每一行存放的都是用户的 query,每个文件的query都可能重复。要求你按照query的频度排序。
解决方案1:hash分解+ 分而治之 +归并
(1)顺序读取10个文件 a0~a9,按照 hash(query)%10 的结果将 query 写入到另外10个文件(记为 b0~b9)中,这样新生成的文件每个的大小大约也1G
(2)找一台内存2G左右的机器,依次使用 hashmap(query, query_count) 来统计每个 query 出现的次数。利用 快速/堆/归并排序 按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件c0~c9。
(3)对这10个文件 c0~c9 进行归并排序(内排序与外排序相结合)。每次取 c0~c9 文件的 m 个数据放到内存中,进行 10m 个数据的归并,即使把归并好的数据存到 d结果文件中。如果 ci 对应的m个数据全归并完了,再从 ci 余下的数据中取m个数据重新加载到内存中。直到所有ci文件的所有数据全部归并完成。
解决方案2:Trie树
如果query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。在这种情况下,可以采用 trie树/hashmap 等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。
问题5:海量数据分布在100台电脑中,请高效统计出这批数据的TOP10
解决思想: 分而治之 + 归并
(1)在每台电脑上求出TOP10,采用包含10个元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆)
(2)求出每台电脑上的TOP10后,把这100台电脑上的 TOP10 合并之后,共1000个数据,在采用堆排序或者快排方式 求出 top10
(注意:该题的 TOP10 是取最大值或最小值,如果取频率TOP10,就应该先hash分解,将相同的数据移动到同一台电脑中,再使用hashmap分别统计出现的频率)
问题6:在 2.5 亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数
解决方案1:hash 分解+ 分而治之 + 归并
(1)2.5亿个 int 类型 hash 到1024个小文件中 a0~a1023,如果某个小文件大小还大于内存,进行多级hash
(2)将每个小文件读进内存,找出只出现一次的数据,输出到b0~b1023
(3)最后数据合并即可
解决方案2 : 2-Bitmap
如果内存够1GB的话,采用 2-Bitmap 进行统计,共需内存 2^32 * 2bit = 1GB内存。2-bitmap 中,每个数分配 2bit(00表示不存在,01表示出现一次,10表示多次,11无意义),然后扫描这 2.5 亿个整数,查看Bitmap中相对应位,如果是00,则将其置为01;如果是01,将其置为10;如果是10,则保持不变。所描完成后,查看bitmap,把对应位是01的整数输出即可。(如果是找出重复的数据,可以用1-bitmap。第一次bit位由0变1,第二次查询到相应bit位为1说明是重复数据,输出即可)
2. Trie树+红黑树+hashmap
Trie树、红黑树 和 hashmap 可以认为是第一部分中分而治之算法的具体实现方法之一。
其中,Trie树适合处理海量字符串数据,尤其是大量的字符串数据中存在前缀时。Trie树在字典的存储,字符串的查找,求取海量字符串的公共前缀,以及字符串统计等方面发挥着重要的作用。
- 用于存储时,Trie树因为不重复存储公共前缀,节省了大量的存储空间;
- 用于以字符串的查找时,Trie树依靠其特殊的性质,实现了在任意数据量的字符串集合中都能以O(len)的时间复杂度完成查找(len为要检索的字符串长度);
- 在字符串统计中,Trie树能够快速记录每个字符串出现的次数
问题1:上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。
解决方案: hashmap/红黑树 + 堆排序
(1)如果是上千万或上亿的 int 数据,现在的机器4G内存能存下。所以考虑采用 hashmap/搜索二叉树/红黑树 等来进行统计重复次数
(2)然后使用包含 N 个元素的小顶堆找出频率最大的N个数据
问题2:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,并给出时间复杂度
解决思路: trie树 + 堆排序
用 trie树 统计每个词出现的次数,时间复杂度是O(n*len)(len表示单词的平均长度)。
然后使用小顶堆找出出现最频繁的前10个词,时间复杂度是O(n*lg10)。
总的时间复杂度,是O(n*len)与O(n*lg10)中较大的那一个。
问题3:有一千万个字符串记录(这些字符串的重复率比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个),每个查询串的长度为1-255字节。请你统计最热门的10个查询串(重复度越高,说明越热门),要求使用的内存不能超过1G。
内存不能超过 1G,每条记录是 255byte,1000W 条记录需要要占据2.375G内存,这个条件就不满足要求了,但是去重后只有 300W 条记录,最多占用0.75G内存,因此可以将它们都存进内存中去。
解决方案1:Trie树
使用 trie树(或者使用hashmap),关键字域存该查询串出现的次数。最后用10个元素的最小堆来对出现频率进行排序。总的时间复杂度,是O(n*le)与O(n*lg10)中较大的那一个。
解决方案2:Hash
第一步、先对这批海量数据预处理,在O(N)的时间内用Hash表完成统计;
第二步、借助堆这个数据结构,找出Top K,时间复杂度为N‘*logK;
即,借助堆结构,我们可以在log量级的时间内查找和调整/移动。因此,维护一个K(该题目中是10)大小的小根堆,然后遍历300万的Query,分别和根元素进行对比所以,我们最终的时间复杂度是:O(N) + N'*O(logK),(N为1000万,N’为300万)。
问题4:1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。
解决方案:trie树
3. BitMap 与 Bloom Filter:
1、BitMap 就是通过 bit 位为 1 或 0 来标识某个状态存不存在。可用于数据的快速查找,判重,删除,一般来说适合的处理数据范围小于 8bit *2^32。否则内存超过4G,内存资源消耗有点多。
2、Bloom Filter 主要是用于判定目标数据是否存在于一个海量数据集 以及 集合求交集。以存在性判定为例,Bloom Filter 通过对目标数据的映射,能够以 O(k) 的时间复杂度判定目标数据的存在性,其中k为使用的hash函数个数。这样就能大大缩减遍历查找所需的时间。
问题1:已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
解决思路:
8位最多99 999 999,需要 100M个bit 位,不到12M的内存空间。我们把 0-99 999 999的每个数字映射到一个Bit位上,这样,就用了小小的12M左右的内存表示了所有的8位数的电话
问题2:2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
解决方案:使用 2-bitmap,详情见上文
问题3:给40亿个不重复的 unsigned int 的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中
解决方案1:Bitmap
使用 Bitmap,申请 512M 的内存,一个bit位代表一个 unsigned int 值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
解决方案2:位图方法
又因为2^32为40亿多,所以给定一个数可能在,也可能不在其中;
Step1:这里把40亿个数中的每一个用32位的二进制来表示,假设这40亿个数开始放在一个文件中。
Step2:然后将这40亿个数分成两类:
1.最高位为0
2.最高位为1
Step3:并将这两类分别写入到两个文件中,其中一个文件中数的个数<=20亿,而另一个>=20亿(这相当于折半了);与要查找的数的最高位比较并接着进入相应的文件再查找
Step4:再然后把这个文件为又分成两类:
1.次最高位为0
2.次最高位为1
Step5:并将这两类分别写入到两个文件中,其中一个文件中数的个数<=10亿,而另一个>=10亿(这相当于折半了);与要查找的数的次最高位比较并接着进入相应的文件再查找。
.......
以此类推,就可以找到了,而且时间复杂度为O(logn),方案2完。
补充:
- 使用位图法判断整形数组是否存在重复
判断集合中存在重复是常见编程任务之一,当集合中数据量比较大时我们通常希望少进行几次扫描,这时双重循环法就不可取了。
位图法比较适合于这种情况,它的做法是按照集合中最大元素max创建一个长度为max+1的新数组,然后再次扫描原数组,遇到几就给新数组的第几位置上1,如遇到5就给新数组的第六个元素置1,这样下次再遇到5想置位时发现新数组的第六个元素已经是1了,这说明这次的数据肯定和以前的数据存在着重复。这种给新数组初始化时置零其后置一的做法类似于位图的处理方法故称位图法。它的运算次数最坏的情况为2N。如果已知数组的最大值即能事先给新数组定长的话效率还能提高一倍。
问题4:现有两个各有20亿行的文件,每一行都只有一个数字,求这两个文件的交集。
解决方案:采用 bitmap 进行问题解决,因为 int 的最大数是 2^32 = 4G,用一个二进制的下标来表示一个 int 值,大概需要4G个bit位,即约4G/8 = 512M的内存,就可以解决问题了。
① 首先遍历文件,将每个文件按照数字的正数,负数标记到2个 bitmap 上,为:正数 bitmapA_positive,负数 bitmapA_negative
② 遍历另为一个文件,生成正数:bitmapB_positive,bitmapB_negative
③ 取 bitmapA_positive and bitmapB_positive 得到2个文件的正数的交集,同理得到负数的交集。
④ 合并,问题解决
这里一次只能解决全正数,或全负数,所以要分两次
问题5:与上面的问题4类似,只不过现在不是A和B两个大文件,而是A, B, C, D….多个大文件,求集合的交集
解决方案:
(1)依次遍历每个大文件中的每条数据,遍历每条数据时,都将它插入 Bloom Filter;
(2)如果已经存在,则在另外的集合(记为S)中记录下来;
(3)如果不存在,则插入Bloom Filter;
(4)最后,得到的S即为所有这些大文件中元素的交集
4. 多层划分:
多层划分本质上还是分而治之的思想,重在“分”的技巧上!因为元素范围很大,需要通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。适用用于:第k大,中位数,不重复或重复的数字
问题1:求取海量整数的中位数
解决方案:
- 依次遍历整数,按照其大小将他们分拣到n个桶中。如果有的桶数据量很小,有的则数据量很大,大到内存放不下了;对于那些太大的桶,再分割成更小的桶;
- 之后根据桶数量的统计结果就可以判断中位数落到哪个桶中,如果该桶中还有子桶,就判断在其哪个子桶中,直到最后找出目标。
问题2:一共有N个机器,每个机器上有N个数,每个机器最多存 N 个数,如何找到 N^2 个数中的中数?
解决方案1: hash分解 + 排序
- 按照升序顺序把这些数字,hash划分为N个范围段。假设数据范围是2^32 的unsigned int 类型。理论上第一台机器应该存的范围为0~(2^32)/N,第i台机器存的范围是(2^32)*(i-1)/N~(2^32)*i/N。hash过程可以扫描每个机器上的N个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第N个区段的数放到第N个机器上。注意这个过程每个机器上存储的数应该是O(N)的。
- 然后我们依次统计每个机器上数的个数,依次累加,直到找到第k个机器,在该机器上累加的数大于或等于(N^2)/2,而在第k-1个机器上的累加数小于(N^2)/2,并把这个数记为x。那么我们要找的中位数在第k个机器中,排在第(N^2)/2-x位。然后我们对第k个机器的数排序,并找出第(N^2)/2-x个数,即为所求的中位数的复杂度是O(N^2)的。
解决方案2: 分而治之 + 归并
先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这N个机器上的数归并起来得到最终的排序。找到第(N^2)/2个便是所求。复杂度是O(N^2 * lgN^2);