海量数据处理方法大总结
方式一:分而治之/hash映射(哈希映射) + hashmap统计 + 快速/归并/堆排序(万能方法)
这种方法是典型的“分而治之”的策略,是解决空间限制最常用的方法,即海量数据不能一次性读入内存,而我们需要对海量数据进行的计数、排序等操作。
基本思路如下图所示:
- 步骤一:先借助哈希算法,计算每一条数据的 hash 值,按照 hash 值将海量数据分布存储到多个桶中。根据 hash 函数的唯一性,相同的数据一定在同一个桶中。
- 步骤二:如此,我们再依次处理这些小文件,最后做合并运算即可。
某搜索公司一天的用户搜索词汇是海量的(百亿级别),请设计一种求出每天热门Top100词汇的可行办法(利用堆)
- 利用哈希函数将大文件分为小文件,用hash统计每个小文件的词频,选出每个小文件的top100;
- 将每个小文件的top100做成大根堆,然后将每个小文件的大根堆的堆顶拿出来
重新做成一个大根堆
,这里称为总堆
; - 将总堆的堆顶元素(这里记为 A)拿出来(这个元素就是所有数据中最大的),看A原本属于哪个大根堆,将原大根堆的A元素弹出,再组成一个大根堆,
然后将该大根堆的堆顶元素加入总堆
- 就这么循环往复,选取出top100
海量日志数据,统计出某日访问百度次数最多的那个IP
思路分析:IP地址最多有 2^32 = 4G 种取值情况,所以不能完全加载到内存中进行处理,采用 hash分流+ 分而治之 + 归并 的方式:
- 按照 IP 地址的 hash(IP)%1024 值,把海量IP日志分别存储到1024个小文件中。
这样每个小文件最多包含4MB个IP地址
; - 对于每一个小文件,利用HashMap统计每个IP出现的次数,然后找出频率最大的IP;
- 然后再在这1024组最大的IP中,找出那个频率最大的IP。
有a、b两个文件,各存放50亿个URL,每个URL各占64字节,内存限制是4G,让你找出a、b文件共同的URL
思路分析:如果内存中想要存入所有的 URL,共需要 50亿 * 64= 320G大小空间,所以采用 hash 分解+ 分而治之 + 归并 的方式:
- 遍历文件a,对每个 URL 根据某种 hash规则,求取 hash(URL)/1024,然后根据所取得的值将 URL 分别存储到1024个小文件(a0 - a1023)中,
这样每个小文件的大约为300M
。- 如果hash结果很集中使得某个文件ai过大,可以再对ai进行二级hash(ai0~ai1024),这样 URL 就被hash到 1024 个不同级别的文件中。
- 分别比较文件,a0 VS b0,…… ,a1023 VS b1023,求每对小文件中相同的 URL 时:
- 把其中一个小文件的 URL 存储到 HashMap 中,然后遍历另一个小文件的每个 URL ;
- 看其是否在刚才构建的 HashMap 中,如果是,那么就是共同的URL ,存到文件中。
- 最后把1024个文件中的相同 url 合并起来。
在 2.5 亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数
思路一:hash 分解+ 分而治之 + 归并
- 2.5亿个 int 类型 hash 到1024个小文件中 a0~a1023,如果某个小文件大小还大于内存,进行多级hash;
- 将每个小文件读进内存,找出只出现一次的数据,输出到 b0~b1023;
- 最后数据合并即可
思路二:BitMap 后面讲
10G的无序整型数给5G内存怎么排序?(重要 重要 重要)
思路分析:小根堆的思想,小根堆存的是 数字 以及这个数字 出现的次数,也就是两个int型整数,一共8字节,但小根堆可能还有一些索引的消耗,所以我们这里按照16字节计算
- 那么5G内存支持 5G / 16B = 335544320 个,到离2的某次方最接近的数字,这里取
2^28 = 268435456
;
方案二:BitMap(位图) 与 Bloom Filter(布隆过滤器)
参考:https://my.oschina.net/freelili/blog/2885263
什么是位图(bitmap)
位图法就是bitmap的缩写。所谓bitmap,就是用每一位来存放某种状态。适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存不存在的。
在java中,一个int类型占32个字节,假设有 32 个int行整数:
- 用一个int数组来表示时为new int[32]总计占用内存
32 * 32 bit
; - 现假如我们用int字节码的每一位表示一个数字的话,那么32个数字只需要一个int类型所占内存空间大小就够了,这样在大数据量的情况下会节省很多内存。
举个例子来说明:假设我们现在有N个数,随机给定一个数,要求你快速找出这个给定的数是否在这N个数中。这个问题其实我们需要思考两个问题:
- 问题一:如何用位图表示这N个数?
- 问题二:如何利用位图来查找给定数是否存在于这N个数之中?
问题一:如何用位图表示这N个数?
1个int占4字节即 4 * 8 = 32位,那么我们只需要申请一个长度为 1+N/32的int型数组即可存储完这些数据,int[] tmp = new int[1+N/32]
,其中N代表要进行查找的总数,tmp 中的每个元素在内存在占32位可以对应表示十进制数0~31,所以可得到BitMap表:
- tmp[0]:可表示0~31
- tmp[1]:可表示32~63
- tmp[2]可表示64~95
- …
那么接下来就看看十进制数如何转换为对应的bit位:
假设这N个int数据为:6,3,8,32,36,…,那么具体的BitMap表示为:
问题二:如何利用位图来查找给定数是否存在于这N个数之中?
如何判断int数字在tmp数组的哪个下标,这个其实可以通过直接除以32取整数部分,例如:
- 整数8除以32取整等于0,那么8就在tmp[0]上。
- 另外,我们如何知道了8在tmp[0]中的32个位中的哪个位,这种情况直接mod上32就ok,又如整数8,在tmp[0]中的第8 mod上32等于8,那么整数8就在tmp[0]中的第八个bit位(从右边数起)。
什么是Bloom Filter(布隆过滤器)
Bloom Filter是一种空间效率很高的随机数据结构,它的原理是:
- 当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位阵列(Bit array)中的K个点,把它们置为1;
- 检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:
- 如果这些点有任何一个0,则被检索元素一定不在;
- 如果都是1,则被检索元素很可能在。
这就是布隆过滤器的基本思想。
但Bloom Filter的这种高效是有一定代价的:
在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些 “零错误”
的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。
给40亿个不重复的的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中(重要 重要)
这个问题其实我们需要思考两个问题:
- 问题一:如何用位图表示这N个数?
- 问题二:如何利用位图来查找给定数是否存在于这N个数之中?
问题一:如何用位图表示这N个数?
1个int占4字节即 4 * 8 = 32位,那么我们只需要申请一个长度为 1+N/32的int型数组即可存储完这些数据,int[] tmp = new int[1+N/32]
,其中N代表要进行查找的总数,tmp 中的每个元素在内存在占32位可以对应表示十进制数0~31,所以可得到BitMap表:
- tmp[0]:可表示0~31
- tmp[1]:可表示32~63
- tmp[2]可表示64~95
- …
那么接下来就看看十进制数如何转换为对应的bit位:
假设这N个int数据为:6,3,8,32,36,…,那么具体的BitMap表示为:
问题二:如何利用位图来查找给定数是否存在于这N个数之中?
如何判断int数字在tmp数组的哪个下标,这个其实可以 通过直接除以32取整数部分
,例如:
整数8除以32取整等于0,那么8就在tmp[0]上
。- 另外,我们如何知道了8在tmp[0]中的32个位中的哪个位,这种情况直接mod上32就ok,又如整数8,在tmp[0]中的第8 mod上32等于8,那么整数8就在tmp[0]中的第八个bit位(从右边数起)。
某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
8位最多99 999 999,大概需要99M个bit,大概10几M字节的内存即可
。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要99M个Bit==1.2MBytes,这样,就用了小小的1.2M左右的内存表示了所有的8位数的电话)
在 2.5 亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数
对于这种场景我可以采用2-BitMap来解决, 即为每个整数分配2bit ,用不同的0、1组合来标识特殊意思,如00表示此整数没有出现过,01表示出现一次,11表示出现过多次
,就可以找出重复的整数了,其需要的内存空间是正常BitMap的2倍,为:2.5亿*2/8/1024/1024=71.5MB
。
具体的过程如下:
- 扫描着3亿个整数,先查看BitMap中的对应位置,如果00则变成01,是01则变成11,是11则保持不变;
- 当将3亿个整数扫描完之后也就是说整个BitMap已经组装完毕;
- 最后查看BitMap将对应位为11的整数输出即可。
32位无符号整数的范围是 0 - 2^32 - 1,现在有一个正好包含40亿个无符号整数的文件,所以在整数范围中必然存在没出现的数。可以最多使用1GB的内存,怎么找到所有未出现的数?
每个数字对应1bit,0表示未出现,1表示出现过,所需内存为:2^32 / 8 / 1024 / 1024 = 512M
。
上一个问题进阶:只给3KB内存,找到40亿个无符号整数中的一个没出现的数即可。(范围统计思想)
- 3KB 内存能创建多大的整形数组:3KB / 4 = 750,找到离2的某次方最接近的数字,这里取 2^9 = 512,创建长度为512的的整形数组:
int[] tmp = new int[512];
; - 0 - 2^32 - 1等量的分为512份一定能整除,2^32 / 512 = 8388608,每份有8388608个数;
- tmp[0]表示 0 ~ 8388608 - 1中的数出现多少次,tmp[1]表示 8388608 ~ 2 * 8388608 - 1中的数出现多少次。。。。。。tmp[511]表示 8388608 * 511 ~8388608 * 512 - 1中的数出现多少次;
x / 8388608 即可定位属于数组中的哪个数,tmp[x / 8388608 ]++
- 只有四十亿个数,所以一定会有一个词频统计的数值不够 8388608,
那么也就知道缺的数字属于哪个范围
; 再将这个范围的数字分为 512 份,重复上面的过程,周而复始,就能得到未出现的数字
。
上一个问题再次进阶:只能申请有限个变量 (TODO 左程云再听一遍)
- 0 - 2^32 - 1中进行二分,如果满了,左右应该都是 2^32 / 2 个数;
- 在不满的区间继续二分,最终一定能将二分的范围定成一个数的范围;
最多二分 32 次
。
32位无符号整数的范围是 0 - 2^32 - 1,现有40亿个无符号整数,可以最多使用1GB的内存,找出出现了两次的数?
思路一:hash 分解+ 分而治之 + 归并
思路二:BitMap
- 每个数分配 2bit,00表示0次,01表示一次,10表示两次,两次及以上几位11;
- 耗费内存:
2^32 * 2 / 8 / 1024 / 1024 = 1024M = 1GB
,正好够用。
现有两个各有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个文件的正数的交集,同理得到负数的交集;
- 合并,问题解决。
这里一次只能解决全正数,或全负数,所以要分两次
解决方案二:Bloom Filter(布隆过滤器)
- 依次遍历每个大文件中的每条数据,遍历每条数据时,都将它插入 Bloom Filter;
- 如果已经存在,则在另外的集合(记为S)中记录下来;
- 如果不存在,则插入Bloom Filter;
- 最后,得到的S即为所有这些大文件中元素的交集
采用 Bloom Filter(布隆过滤器)会有一定的错误率
上一个问题进阶:现在不是A和B两个大文件,而是A, B, C, D….多个大文件,求集合的交集
- 依次遍历每个大文件中的每条数据,遍历每条数据时,都将它插入 Bloom Filte;
方法三:多层划分(范围统计思想)
多层划分本质上还是分而治之的思想,重在“分”的技巧上!因为元素范围很大,需要通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。适用于:第k大,中位数,不重复或重复的数字
只给3KB内存,找到40亿个无符号整数中的一个没出现的数即可。(范围统计思想)
- 首先根据内存来计算能申请无符号整形的最大数量:3KB 内存能创建多大的整形数组:3KB / 4 = 750,找到离2的某次方最接近的数字,这里取 2^9 = 512,创建长度为512的的整形数组:
int[] tmp = new int[512]
; - 0 - 2^32 - 1等量的分为512份一定能整除,2^32 / 512 = 8388608,每份有8388608个数;
- tmp[0]表示 0 ~ 8388608 - 1中的数出现多少次,tmp[1]表示 8388608 ~ 2 * 8388608 - 1中的数出现多少次。。。。。。tmp[511]表示 8388608 * 511 ~8388608 * 512 - 1中的数出现多少次;
x / 8388608 即可定位属于数组中的哪个数,tmp[x / 8388608 ]++
- 只有四十亿个数,所以一定会有一个词频统计的数值不够 8388608,
那么也就知道缺的数字属于哪个范围
; 再将这个范围的数字分为 512 份,重复上面的过程,周而复始,就能得到未出现的数字
。
最多使用10MB内存,怎么找到40亿个整数的中位数?(范围统计思想)
- 首先根据内存来计算能申请无符号整形的最大数量:10KB 内存能创建多大的整形数组:10KB / 4 = 2500,找到离2的某次方最接近的数字,这里取 2^11 = 2048,创建长度为512的的整形数组:
int[] tmp = new int[2048]
; - 将40亿个整数分为2048份,2^32 / 2048 = 2097152,每份有2097152个数。tmp[0]表示0 ~ 2097152 - 1中的数出现多少次,,tmp[1]表示 2097152 ~ 2 * 2097152 - 1中的数出现多少次。。。。。。tmp[2047]表示 2097152 * 2047 ~2097152 * 2048 - 1中的数出现多少次;
- 遍历这40亿个数进行词频统计:arr[x / 2048]++;
- 对词频统计数组进行累加,tmp[0] + tmp[1] + 。。。+ tmp[n],一直加到刚好大于等于20亿,这样我们就知道中位数(也就是第20亿数)出现在哪个范围上,这里假设是 tmp[m];
我们再将tmp[m]等分为2048份,重复上面的过程,周而复始,就能得到未出现的数字
大数据小内存排序问题:10G的有符号整数给5G内存怎么排序?(重要 重要 重要)
- 无符号整数范围:2^32
- 有符号整数范围:-2^31 ~ 2^31 - 1,总共也是2^32个数
方法一:小根堆(范围统计)
思路分析:小根堆的思想,小根堆存的是 数字 以及这个数字 出现的次数,也就是两个int型整数,一共8字节,但小根堆可能还有一些索引的消耗,所以我们这里按照16字节计算。
- 那么5G内存支持 5G / 16B = 335544320 个,到离2的某次方最接近的数字,这里取
2^28 = 268435456
; - 无符号整数的范围时:2^32 ,可以将 2^32这个范围分为:2 ^32 / 2^28 = 4,也就是分成四个范围
- 第一个范围:-2^31 ~ -2^31 + 2^28 - 1,。。。。。,第四个范围:-2^31 + 2^28 *3 ~ 2^31 - 1
依次用小根堆统计每个范围内每个数字出现的次数
,也就是每次搞定一个范围的词频统计,然后输出到文件中。
方法一的思考:如果有数字大量重复,比如a出现了5亿次,会有影响吗
就算a出现了5亿次,也只是词频变为5亿而已,词频也只是用一个int型来存储,不占内存
方法二:大根堆(维护一个门槛,即堆顶元素)
假设有10亿个数,那么我们根据内存大小维护一个大根堆,比如这里我们只能维护一个能存储500万条数据的大根堆,大根堆中存的数据和方法一中的一样,存的是 数字 以及这个数字 出现的次数,也就是两个int型整数,一共8字节。
- 建立一个能存储500万条数据的大根堆,我们遍历这10亿个数字,找到最小的500万个数字;
- 先往大根堆中存入500万个数,此时堆顶就是最大的数;
- 遍历剩余的数,当有比堆顶元素小的数,那么就弹出堆顶元素,然后将该数字入堆,这样遍历完就找到了最小的500万个数。
- 记录此时大根堆的堆顶元素,堆顶M,然后将大根堆中这500万条记录输出到文件中;
- 再次遍历这10亿个数字,但是小于等于M的数忽略,就这么循环往复,就能完成排序。
为了方便理解,我们举一个只有十个数字的例子,并且维护一个只能存三个数字的大根堆,利用上面提到的大根堆的思想进行排序
- 建立一个能存3条数据的大根堆,我们遍历这10个数字,找到最小的3个数字;
- 先往大根堆中存入3个数,此时堆顶就是最大的数;
- 遍历剩余的数,当有比堆顶元素小的数,那么就弹出堆顶元素,然后将该数字入堆,这样遍历完就找到了最小的3个数。
- 记录此时大根堆的堆顶元素,堆顶M,然后将大根堆中这3条记录输出到文件中;
- 再次遍历这10亿个数字,但是小于等于M的数忽略,就这么循环往复,就能完成排序。
参考:https://blog.csdn.net/a745233700/article/details/114006686
参考:https://blog.csdn.net/v_JULY_v/article/details/7382693
参考:https://www.bilibili.com/video/BV13g41157hK?p=15&spm_id_from=333.880.my_history.page.click