问题引入
我有40亿个无序int类型的整数,再给一个新的整数,我需要判断新的整数是否在40亿个整数中,你会怎么做?
问题分析
最常规直观的解法,就是使用一个HashSet,将所有数都add进去,然后对要判断的数,执行一下contains函数判断下就ok了。
但是思考一下,这个解法有可行性吗?考虑一下这么做的话,这个HashSet会占用多少内存?
正常一个int整数,占用4个字节。
40亿个整数的话,就是 4,000,000,000 * 4字节 = 16,000,000,000字节 ≈ 14.9G。
我们是不是得申请一个特别大内存的机器,专门做这个运算才行?
假如我们只有一台2G的机器的话,如何实现呢?
BitMap登场
我们申请一个Bit数组,数组的每个元素,都能表示0或者1,数组的长度为2^32。
初始数组:
假设40亿的整数分别为 4,7,1,5,9 …
然后,对于40亿中的每个整数,我们都将这个整数作为下标,把数组中对应的位置为1。
如图所示:
由于一个整数占4字节,所以一个无符号整数,取值范围是0 ~ (2^32 - 1)。
因此,对于40亿整数中的任何一个数,都可以对应放进这个数组里面。
另外,可以简单的计算出来,这个数组,占用的空间大小为:
2^32 Bit = 2^32 (Bit) / 8 (Byte) / 1024 (KB) / 1024 (M) = 512M
这时,我们想要判断一个整数是否在这40亿整数中,我们只需要直接在数组中,取该数字下标的值。
如果值是1则存在,如果值是0则不存在。
是不是很简单?
对于该问题,利用BitMap,我们可以将计算时,近15G的空间占用,变成512M。
而时间复杂度不变,依然是O(N)。
总结
通过以上部分,我们知道了,BitMap可以实现海量数字中的快速定位查找,并且极其节约空间。
扩展延伸
思考以下问题:
根据微信最近公布的数据,微信App的月活跃已经破11亿了。
假设微信做了一个每日签到的功能。
签到数据按天分表存储,也就是说每一天的签到数据,都独立存在于一个表里。
这时,如果我们接到了2个查询数据的需求,分别要给出五一3天假里面:
A、3天连续打卡了的用户id集合
B、3天内任一天打过卡的用户id集合
要求用户id不重复,你会如何实现呢?
每天的打卡数据,可能都是十亿级别的。
如果把三天的用户id数据,都load到内存里的集合对象里面,再挨个对比判断,那依然会出现内存溢出的情况。
细心的同学可能会想到,是不是也可以用BitMap实现呢?答案是可以的。
我们将三天的数据,都维护在一个BitMap里,每个BitMap占用的空间差不多也是百M级别,完全能接受。
然后对于三个Bit数组,我们可以高效的执行 按位与 和 按位或 操作。
按位与出来的结果集,即为A查询的结果,3天都打卡的用户id集合。
按位或出来的结果集,即为B查询的结果,3天内任一天打过卡的用户id集合。
总结
所以,BitMap也可以利用位运算,高效的实现海量数据集合的相融和相交等操作。
Java实现和源码解析
JDK中,是直接提供的BitMap集合的实现类的:java.util.BitSet.class
基础用法如下:
BitSet bitSet = new BitSet();
bitSet.set(0, Boolean.TRUE);
bitSet.set(3, Boolean.TRUE);
boolean b1 = bitSet.get(0);
boolean b2 = bitSet.get(2);
查看BitSet.class的源码,可以发现,内部是使用long数组作为存储的。
每个long元素,可以表示8字节,也就是64位二进制数字。
每次set时,根据下标,确定到long数组的位置,先判断是否需要扩容。
然后将定位到的long数字,根据位运算,将指定位,置为0或者1。
每次get时,同样是根据下标,确定到long数组的位置,利用位运算,取出对应位的值即可。
使用long类型作为数组存储的话,虽然并不能降低空间消耗,但是在对BitMap做集合操作时,可以降低数组的循环次数。
BitMap的未来
BitMap的功能无疑是很强大的,相较于直接存储数字,空间占用率会成数十倍的降低。
但是在现有互联网海量数据的背景下,十亿的数据,可能很轻易就会达到且超过了。
如果数据量达到百亿,甚至千亿的级别下,那又将达到单机处理能力的瓶颈。
这里会有几种优化方向:
BitMap压缩技术
如果将BitMap保存到外部存储(数据库或者文件),计算时从外部存储加载到内存,这种情况下,存储的BitMap越大,需要的外部存储空间就越大;并且IO消耗也会更大。
这时,可以选择用gzip等文件压缩技术,在存储时,将BitMap文件进行压缩,在加载时再进行解压。
可以降低存储空间和IO消耗,但是代价是压缩和解压缩时,会消耗更多的CPU资源。
其次,对BitMap自身,也是有一些压缩技术的。
BitMap存储的,实际上是一些连续的0集合和1集合。那么我们是可以对于大段的0集合和1集合,进行合并。
例如,对于00000000111111111111000011111而言,可以存储为0,8,1,12,0,4,1,6。
也就是,8个0,12个1,4个0,6个1。
在海量存储里面,可能会有极大段的连续相同数据,用该压缩技术,可以极大降低BitMap自身的占用空间。
分布式计算
在使用BitMap做集合操作时,假如BitMap的数据特别巨大,比如一年的打卡数据,甚至更多,那么单机的计算能力和存储能力,就完全成为瓶颈。
这时,其实是可以引入分布式计算的。
分布式计算的理论基础:
在集合代数里面,集合的交、并、差、补操作,是满足交换律、分配率和结合律的。详见集合代数
基于这些集合代数计算的原理,我们可以把复杂的多维交叉分析,分解成很多小单元计算,分配到不同的服务器上计算,再做汇总计算得到结果。
总结
BitMap在某些应用领域,使用起来会很强大。但是也会有一些限制弊端,需要我们去考虑到。