海量数据场景下的算法问题:透彻理解从40个亿中产生一个不存在的整数
Posted 纵横千里,捭阖四方
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了海量数据场景下的算法问题:透彻理解从40个亿中产生一个不存在的整数相关的知识,希望对你有一定的参考价值。
在大部分算法中,默认给定的数据量都很小的,例如只有几个或者十几个元素,但是如果将数据量提高到百万甚至十几亿,那处理逻辑就会发生很大差异,这也是算法考查中,经常出现的一类问题。此时普通的数组、链表、Hash、树等等结构有无效了 ,而常规的递归、排序,回溯、贪心和动态规划等思想也无效了,必须另外想办法。这类问题该如何下手呢?位运算以及相关拓展用法可以解决一部分场景的问题,本文就分析一个最热门的问题之一:从40个亿中产生一个不存在的整数。
题目要求:给定一个输入文件,包含40亿个非负整数,请设计一个算法,产生一个不存在该文件中的整数,假设你有1GB的内存来完成这项任务。
-
进阶:如果只有10MB的内存可用,该怎么办?
本题不用写代码,如果能将方法说清楚就很好了 ,我们接下来一步步分析该如何做。
1 位图存储大数据的原理
假设用哈希表来保存出现过的数,如果 40 亿个数都不同,则哈希表的记录数为 40 亿条,存一个 32 位整数需要 4B,所以最差情况下需要 40 亿*4B=160 亿字节,大约需要16GB 的空间,这是不符合要求的。
如果数据量很大,采用位方式(俗称位图)存储数据是常用的思路,那位图如何存储元素的呢? 我们可以使用 bit map 的方式来表示数出现的情况。具体地说, 是申请一个长度为 4 294 967 295 的 bit 类型的数组 bitArr(就是boolean类型),bitArr 上的每个位置只可以表示 0 或1 状态。8 个bit 为 1B,所以长度为 4 294 967 295 的 bit 类型的数组占用 500MB 空间,这就满足题目给定的要求了。
那怎么使用这个 bitArr 数组呢?就是遍历这 40 亿个无符号数,遇到所有的数时,就把 bitArr 相应位置的值设置为 1。例如,遇到 1000,就把bitArr[7000]设置为 1。
遍历完成后,再依次遍历 bitArr,看看哪个位置上的值没被设置为 1,这个数就不在 40 亿个数中。例如,发现 bitArr[8001]==0,那么 8001 就是没出现过的数,遍历完 bitArr 之后,所有没出现的数就都找出来了。
位存储的核心是:我们存储的并不是这40亿个数据本身,而是其对应的位置。这一点明白的话,整个问题就迎刃而解了。
2 使用10MB来存储
如果现在只有 10MB 的内存,此时位图也不能搞定了,我们要另寻他法。这里我们使用分块思想,时间换空间,通过两次遍历来搞定。
如果只有10MB,我们只要求找到其中一个没出现过的数即可。
首先,将0~4 294 967 295(2^32) 这个范围是可以平均分成 64 个区间的,每个区间是 67 108 864 个数,例如:
-
第0 区间(0~67 108 863)
-
第 1 区间(67 108 864~134 217 728)
-
第 i 区间(67 108 864´I~67 108 864´(i+1)-1),
-
……,
-
第 63 区间(4 227 858 432~4 294 967 295)。
因为一共只有 40 亿个数,所以,如果统计落在每一个区间上的数有多少,肯定有至少一个区间上的计数少于67 108 864。利用这一点可以找出其中一个没出现过的数。具体过程是通过两次遍历来搞定:
第一次遍历,先申请长度为 64 的整型数组 countArr[0..63],countArr[i]用来统计区间 i 上的数有多少。遍历 40 亿个数,根据当前数是多少来决定哪一个区间上的计数增加。例如,如果当前数是 3 422 552 090 , 3 422 552 090/67 108 864=51 , 所以第 51 区间上的计数增加countArr[51]++。遍历完 40 亿个数之后,遍历 countArr,必然会有某一个位置上的值(countArr[i]) 小于 67 108 864,表示第 i 区间上至少有一个数没出现过。我们肯定会找到至少一个这样的区间。
此时使用的内存就是countArr 的大小(64*4B),是非常小的。
假设找到第 37 区间上的计数小于 67 108 864,那么我们对这40亿个数据进行第二次遍历:
-
申请长度为 67 108 864 的 bit map,这占用大约 8MB 的空间,记为 bitArr[0..67108863]。
-
遍历这 40 亿个数,此时的遍历只关注落在第 37 区间上的数,记为 num(num满足num/67 108 864==37),其他区间的数全部忽略。
-
如果步骤 2 的 num 在第 37 区间上,将 bitArr[num - 67108864*37]的值设置为 1,也就是只做第 37 区间上的数的 bitArr 映射。
-
遍历完 40 亿个数之后,在 bitArr 上必然存在没被设置成 1 的位置,假设第 i 个位置上的值没设置成 1,那么 67 108 864´37+i 这个数就是一个没出现过的数。
总结一下进阶的解法:
-
根据 10MB 的内存限制,确定统计区间的大小,就是第二次遍历时的 bitArr 大小。
-
利用区间计数的方式,找到那个计数不足的区间,这个区间上肯定有没出现的数。
-
对这个区间上的数做 bit map 映射,再遍历bit map,找到一个没出现的数即可。
3 如何确定分块的区间
在上面的例子中,我们看到采用两次遍历,第一次将数据分成64块刚好解决问题。那我们为什么不是128块、32块、16块或者其他类型呢?
这里主要是要保证第二次遍历时每个块都能放进这10MB的空间中。2^23<10MB<2^24,而2^23=8388608大约为8MB,也就说我们一次的分块大小只能为8MB左右。在上面我们也看到了,第二次遍历时如果分为64块,刚好满足要求。
所以在这里我们最少要分成64块,当然如果分成128块、256块等也是可以的。
以上是关于海量数据场景下的算法问题:透彻理解从40个亿中产生一个不存在的整数的主要内容,如果未能解决你的问题,请参考以下文章