文章目录
- 0、资源限制技巧汇总
- 1、题目一:40亿个数,内存限制为1G,如何找到出现次数最多的数
- 2、题目二:40亿个数,内存限制为10MB,找到所有未出现过的数
- 进阶问题1:40亿个数,内存限制为3KB,但是只用找到一个没出现过的数即可
- 进阶问题2:40亿个数,只用有限的变量,找到一个没有出现过的数即可
- 3、题目三:100亿个URL,找出所有重复的URL
- 4、题目四:40亿个数,内存限制为1GB,找出所有出现了2次的数
- 5、题目五:40亿个数,内存限制为3KB,找到这40亿个数的中位数
- 6、题目六:内存限制5GB,给10GB大小的文件排序
- 7、题目七:求出一个给定的大文件中出现次数最多的前 100 名
0、资源限制技巧汇总
- 布隆过滤器用于集合的建立与查询,并可以节省大量空间
- 一致性哈希解决数据服务器的负载管理问题
- 利用并查集结构做岛问题的并行计算
- 哈希函数可以把数据按照种类均匀分流
- 位图解决某一范围上数字的出现情况,并可以节省大量空间
- 利用分段统计思想、并进一步节省大量空间
- 利用堆、外排序来做多个处理单元的结果合并
1、题目一:40亿个数,内存限制为1G,如何找到出现次数最多的数
题目描述
我们都知道,32位无符号整数的范围是 0 0 0 ~ 4 , 294 , 967 , 295 4,294,967,295 4,294,967,295(即 0 0 0 ~ 2 32 − 1 2^{32} - 1 232−1)。
现在有一个正好包含 40 亿个无符号整数的文件,可以使用最多 1GB 的内存,怎么找到出现次数最多的数?
分析
【思路1】
直接提取40亿个整数放入一个数组中进行排序,但是40亿无符号整数,占用内存大小是 160亿Bytes = 16GB,但是题目只给定了 1GB 的内存,所以不可能将所有的数都提取出来进行排序,这种方案不可行。
【思路2】
使用哈希表,而哈希表到底要使用多少内存,和数字的个数无关,而和到底出现了多少种不同的数字有关。
假设 40 亿个数全部都是 1,那么哈希表中只有一条记录(1,40亿)
,即记录了1出现的次数为40亿,其中 key 和 value 都是无符号整数,所以仅需要 8 字节。
最极端的情况,是这 40 亿个数全都不同,因为数字个数 < 无符号整数范围,所以这种可能性是存在的。这样的话,哈希表需要的空间就是 40 40 40亿 × 8 = 320 \times 8 = 320 ×8=320亿Bytes = 32 = 32 =32GB,甚至不如直接提取所有数字进行排序占用的空间少,更不必说哈希表中可能有一些数据结构连接的索引开销。所以题目的 1GB 内存肯定是不行的,哈希表会爆掉。
【思路3】
如果1GB内存全部用来做哈希表,每条记录(数字, 出现次数)占用 8字节内存,则可以存储 1 G B / 8 ≈ 125 , 000 , 000 1GB/8 ≈ 125,000,000 1GB/8≈125,000,000 条记录,但是可能存在其他开销,则保守点估计 1GB 内存只能存储 1亿 条记录,超过 1亿 哈希表就爆掉。(也可以更保守点,存更少的记录)
题目给定的大文件中的40亿个数 / 1亿 = 40,然后处理40亿个数,每个数通过哈希函数得到一个哈希值,然后 模40,根据结果放到这40个不同的文件中。例数字17,假设 h a s h ( 17 ) % 40 = 3 hash(17) \% 40 = 3 hash(17)%40=3,则将17这个数字发送到 3 文件。
因为哈希函数具有均匀性,在模操作后依然具有均匀性,所以可以认为40亿个数即便全部都不同,也会几乎均分地放到各自的文件中。
如此一来,就有 40 个文件,每个文件中的数字个数不知道,但是每个文件中数字种类数绝对不会太超过1亿,哪怕超过,也只会超过一点点。而且同一种数只会被分到同一个文件中。
接着对0号文件使用哈希表求其中出现次数最多的数字,然后将哈希表空间释放去求 1 号文件中出现次数最多的数字,又将哈希表空间释放去求 2 号文件中出现次数最多的数字,…,这样就得到了 40 个文件中各自出现的次数最多的数字,最后看这 40 个最多次数里的Top1。
这就是哈希函数把数据按照种类均匀分流的应用。
推广
如果有一个大文件,但是给定一个限制的内存,要在大文件中找到某个内容出现次数最多。
原大文件中的数据通过哈希函数 f1
,模 m1
,根据结果放到若干个不同的小文件中;若每个小文件的内存仍然大于限制的内存,则通过哈希函数 f2
,模 m2,根据结果放到若干个更小的文件中,一直到小小文件的内存达到限制的内存。
然后再用HashMap对每个小小文件进行处理(每次处理下一个文件时,要将原先的哈希表空间释放掉,即空间复用),找出每个小小文件中的出现次数最多的内容,最后在找出的结果里找到最大值即可。
总结来说,就是不断哈希,不断取模,以达到限制的内存要求。
2、题目二:40亿个数,内存限制为10MB,找到所有未出现过的数
题目描述
我们都知道,32位无符号整数的范围是 0 0 0 ~ 4 , 294 , 967 , 295 4,294,967,295 4,294,967,295(即 0 0 0 ~ 2 32 − 1 2^{32} - 1 232−1)。
现在有一个正好包含 40 亿个无符号整数的文件,可以使用最多 1GB 的内存,怎么找到所有未出现过的数?
分析
位图的应用。
【思路1】
如果用哈希表HashSet统计每种数是否出现过,需要的空间是 40亿 × \times × 4 = 160亿字节 = 16GB,然后再从头开始遍历每种数字是否出现。这种方法已经超过了限制的内存1GB,不可取。
【思路2】
使用位图。
准备一个 bit 类型的数组,因为32位无符号整数的范围是 0 0 0 ~ 2 32 − 1 2^{32} - 1 232−1,那就准备 2 32 2^{32} 232 个 bit,那么实际占用的空间是 2 32 / 8 2^{32} / 8 232/8 Byte < 1 < 1 <1GB,符合题目要求。即用 bit 标记数字是否出现过,出现过为1,没出现过为0。
如何实现 bit 数组呢?
用基础数据类型来构建。比如申请了一个整型数组 int arr[10],每个元素都是int类型,占 32 位,则相当于申请了一个长度为 320 的bit数组。
那么当访问到数字 i i i 时,那就是 a r r [ i / 32 ] arr[i/32] arr[i/32] 这个元素应该填充bit位,然后将 i % 32 i \% 32 i%32 就能知道应该将 a r r [ i / 32 ] arr[i/32] arr[i/32] 的第 i % 32 i \% 32 i%32位 bit 修改为1。
public class Test {
public static void main(String[] args) {
int[] arr = new int[10];
//arr[0] .... arr[9] 都是int类型,有32位bit
//提取bit的第179位
int i = 179;
//提取第179位的状态,即取出 arr[i/32] 这个数字的第 i%32 位
int status = (arr[i/32] & (1 << (i%32))) == 0 ? 0 : 1;
}
}
总结一下整个过程:准备一个大位图( 2 32 2^{32} 232 bit) ,遍历40亿个数的这个文件,遍历到每个数字就将位图中相应的位置标记状态为1,遍历完整个文件后,位图中哪位为 0,就是哪个数字没有出现过。
这是 位图解决某一范围上数字的出现情况 的应用。
进阶问题1:40亿个数,内存限制为3KB,但是只用找到一个没出现过的数即可
【思路】
3KB的内存如果都用来做无符号的整型数组,数组最长能有
3000
/
4
=
750
3000/4 = 750
3000/4=750 的长度。
然后找一个离 750 最近的 2 的某次方,即 2 9 = 512 2^9 = 512 29=512,那如果无符号整型数组申请 512 长度是不会超过限制内存的,即int arr[512],不会爆掉。
无符号整型整数的范围是 0 0 0 ~ 2 32 − 1 2^{32} - 1 232−1,整个范围是 2 32 2^{32} 232,将这个范围均分为 512 份,即 2 32 / 512 = 8388608 2^{32} /512 = 8388608 232/512=8388608,每份包含的范围是一样大的。也就是第一份负责统计 0 ~ 8388608这个范围的数字出现的次数,第二份统计 8388609 ~ 8388609+8388608 这个范围出现的次数,以此类推。又因为数组长度为 512,所以 arr[0] 统计第一份的范围每个数字出现几个(用位信息统计),arr[1]统计第二份的范围每个数字出现的次数,以此类推。
而提给定的数的个数是40亿,而整数范围是0~约43亿,那么就会导致某个区间的词频统计不到 8388608(注意是词频统计,不是数字个数!!),找到那个不满 8388608 的小区间,在这个小区间中找到没有出现过的数即可。而在这个小区间中怎么找没有出现过的数呢?将小区间分成 512 份更小的区间,在这个更小区间中一定又有不满的,又将该不满的区间分成512份,只需几次就能定位到没有出现过的数。
总结:3KB内存的情况时,进行 512 分。
进阶问题2:40亿个数,只用有限的变量,找到一个没有出现过的数即可
一开始,准备三个变量, L L L 在0位置, R R R 在 2 32 − 1 2^{32}-1 232−1 位置, m i d mid mid 在 L L L ~ R R R 中间的位置,如此一来, L L L ~ m i d mid mid 和 m i d mid mid ~ R R R 范围都应该是 2 31 2^{31} 231,但是因为只有 40 亿个数,那么必然存在左右两侧其中一侧范围词频统计不足 2 31 2^{31} 231 的情况,将不足的这一侧范围又进行二分,递归该过程,直到找到没有出现过的数。
总结:有限几个变量的时候,进行二分。
两个进阶问题都用到了分段统计思想。
3、题目三:100亿个URL,找出所有重复的URL
题目描述
有一个包含 100 亿个URL 的大文件,假设每个 URL 占用 64B,请找出其中所有重复的 URL。
【补充】某搜索公司一天的用户搜索词汇是海量的(百亿数据量),请设计一种求出每天热门Top100词汇的可行办法。
分析
要找URL是否重复,可以用布隆过滤器,但是如果不允许有失误率,用哈希函数分流的方法。
先将大文件中的每个URL通过哈希函数分到小文件中,如果小文件还大,再用第二个哈希函数将其分到更小的文件中,以此类推。因为同一个URL是会进同一个文件的,所以检查每个小文件中是否有重复的URL即可。
4、题目四:40亿个数,内存限制为1GB,找出所有出现了2次的数
题目描述
32位无符号整数的范围是 0 0 0 ~ 4 , 294 , 967 , 295 4,294,967,295 4,294,967,295,现在有 40 亿个无符号整数,可以使用最多 1GB 的内存,找出所有出现了两次的数。
分析
用两个比特位信息表示一个数出现的次数。最后看哪两个比特位的结果是10,就表示这个数出现了 2 次。
这种方法实际使用的内存是 2 32 / 8 × 2 > 1 2^{32} / 8 \times 2 > 1 232/8×2>1GB,超过了限制的1GB内存,就使用位图 + 分段统计的方法。先用位图计算 0 0 0 ~ 2 31 2^{31} 231 范围数字的出现次数,然后计算 ( 2 31 + 1 ) (2^{31} + 1) (231+1) ~ ( 2 32 − 1 ) (2^{32} - 1) (232−1) 范围,第一次遍历如果发现是后半范围的数据,则不管。
5、题目五:40亿个数,内存限制为3KB,找到这40亿个数的中位数
题目描述
32位无符号整数的范围是 0 0 0 ~ 4 , 294 , 967 , 295 4,294,967,295 4,294,967,295,现在有 40 亿个无符号整数,可以使用最多 10MB 的内存,怎么找到这 40 亿个整数的中位数。
分析
实现使用分段统计思想。
3KB可以 750 个整型数据,离 750 最近的 2的某次方是 2 9 = 512 2^9 = 512 29=512。
将 0 ~ 2 32 2^{32} 232-1 这个范围均分为 512 份(或叫区间),然后申请一个长度为 512 的整型数组 int arr[512]。
那么能用 arr 统计出每个范围上数字出现的次数。
40亿个数,那么中位数就是第 20 亿个数。
假设刚刚分出的 512 个区间中的第一个区间范围的数字有1亿个,因为要找到第20亿个数,那么就知道中位数绝对不是在这个区间里;假设第二个区间范围的数字出现了 4 亿个,则两个区间数字个数为 5 亿,则就知道中位数既不来自第一个区间,也不来自第二个区间;一直将每个区间范围的数字出现次数依次相加,直到加到某个区间 x x x 后数字个数为 19 亿,再加区间 x + 1 x + 1 x+1 的数字个数到达了 23 亿,那么中位数一定来自 x + 1 x +1 x+1 这个区间,且该区间中的第 1 亿个数就是中位数。 接着再遍历一遍文件,只关心 x + 1 x+1 x+1 这个范围的数字,找到第 1 亿个数即可。
举个小例子:假设要找的中位数是排好序后的第 200 个数
将每个区间的数字个数相加,到30 ~ 39 这个范围时总个数才超过200,所以中位数在 30 ~ 39 这个范围中;
且前三个区间个数相加为117,则30~39这个范围的第83小的数就是中位数;
然后再遍历一遍文件,只关心 30 ~ 39 这个范围的数字,找到第83小的数即可。
6、题目六:内存限制5GB,给10GB大小的文件排序
题目描述
32位无符号整数的范围是 0 0 0 ~ 4 , 294 , 967 , 295 4,294,967,295 4,294,967,295。
有一个 10G 大小的文件,每一行都装着这种类型的数字,整个文件是无序的,给你 5G 的内存空间,请你输出一个 10G 大小的文件,就是原文件所有数字排序的结果。
分析
该题使用堆来做多个处理单元的结果合并。
先举个极端的例子:10G的文件,只能保存三条记录的容器,如何排序。
10G的文件,有一个容器可以保存 3 条记录 “(值,出现的次数)”, 当容器中的3条记录满了时,接下来遍历到的数 x x x,将 x x x 与容器中最大的值 m a x max max 比较,如果 x < m a x x < max x<max,则淘汰容器中的最大值那条记录,将 x x x 放入容器中。 这样的结果就是,当遍历完10G文件后,最小的前三名及其出现的次数都正确统计了。
假设遍历完一遍后,容器中存储的三条记录是 ( a , c n t 1 ) 、 ( b , c n t 2 ) 、 ( c , c n t 3 ) (a, cnt1)、(b, cnt2)、(c, cnt3) (a,cnt1)、(b,cnt2)、(c,cnt3)(其中 a < b < c a < b < c a<b<c),就生成一个文件 file 其中 a a a 写 c n t 1 cnt1 cnt1 遍, b b b 写 c n t 2 cnt2 cnt2 遍, c c c 写 c n t 3 cnt3 cnt3 遍,然后用一个变量 t t t 记录其中的最大值 c c c,即 t = c t = c t=c,释放掉容器中的空间,再遍历一遍 10G 的文件,但是 ≤ c \le c ≤c 的数忽略,就能得到 > 13 >13 >13 的最小前三名,假设遍历完10G文件后容器中的记录是 ( d , c d ) 、 ( e , c e ) 、 ( f , c f ) (d, cd)、(e, ce)、(f, cf) (d,cd)、(e,ce)、(f,cf)(其中 d < e < f ) d < e < f) d<e<f),则接着在文件 file 后面写 c d cd cd 遍 d d d, c e ce ce 遍 e e e, c f cf cf 遍 f f f,写完之后将 t t t 更新成 f,即 t = f t = f t=f,又将容器中的空间释放,再遍历 10G 的文件,重复上述的操作直到容器中不满 3 条记录为止。那么最终文件 file 就是原文件排序后的结果。
现在题目给定的是 5GB内存,就计算容器利用这个 5GB 内存可以存放多少条记录,然后执行上面的操作。
这个容器就是使用的 大根堆。
7、题目七:求出一个给定的大文件中出现次数最多的前 100 名
分析
先将大文件使用哈希函数拆成若干个小文件,然后每个小文件中求Top100。
举个简易的例子——求出现次数最多的Top3。
如大文件使用哈希函数拆成 3 个小文件。
文件1的Top3:
数字 | 出现次数 |
---|---|
a | 17 |
b | 16 |
c | 10 |
文件2的Top3:
数字 | 出现次数 |
---|---|
X | 23 |
Y | 21 |
P | 6 |
文件3的Top3:
数字 | 出现次数 |
---|---|
z | 9 |
k | 9 |
F | 9 |
如何在这多个 Top3 中选出最终的 Top3 呢?
方法1:直接外排序。先三个 Top1 比较,得到 (X, 23) 最大,就是最终的 No.1,然后将它从比较数组中除去;然后此时每个文件中的Top1开始比较,得到 (Y, 21)最大,就是最终的 No.2,将它从比较数组中去除;接着又将此时每个文件中的 Top1 比较,得到 (a, 17)最大,就是最终的 No.3。
方法2:堆上堆。每个文件中的 Top3 组成大根堆,每个大根堆的堆顶再组成总的大根堆,则总的大根堆的堆顶就是最终的No.1,将总的大根堆的堆顶(X, 23)弹出,这个弹出的记录来自文件2,将文件2的下一个记录 (Y, 21) 放到总的大根堆,重新调整堆,此时的堆顶(Y, 21)就是最终的No.2,周而复始,找到TopK个。
最终的Top3 :
数字 | 出现次数 |
---|---|
X | 23 |
Y | 21 |
a | 17 |