BitMap 是一种数据结构, BitMap 用 bit 位来标记某个元素对应的 Value,而 Key 即是该元素; 由于采用了bit为单位来存储数据,因此可以大大节省存储空间(重点)。
1. 什么是BitMap?
计算机中,最小单位是bit,一个bit值只有两个:1、0
我们平时所使用的类型也都是bit构成的,例如 byte字节,一个字节等于8个bit
类型 | 字节(byte) | bit |
---|---|---|
int | 4 byte | 4 * 8 |
short | 2 byte | 2 * 8 |
long | 8 byte | 8 * 8 |
byte | 1 byte | 1 * 8 |
float | 4 byte | 4 * 8 |
double | 8 byte | 8 * 8 |
boolean | 1 byte | 1 * 8 |
char | 2 byte | 2 * 8 |
2. BitMap如何表示
在BitMap中如何表示一个数?
按照上面说的,一个bit位只有两个值:1(存在) 和 0(不存在)
表示{1,2,4,6}这四个数,如以下表示
bit值 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
下标的每一位,表示一位数,0不存在、1存在。(根据’下标’,取出bit值为1的,即为要表示的数字)
计算机内存分配的最小单位是字节,也就是8位,如果要表示{12,13,15}怎么办呢?
这就需要再另一个byte上表示,类似二维数组,第一组数组表示:0 ~ 7 , 第二组数组表示:8 ~ 15
tmp[0] | bit值 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
tmp[1] | bit值 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|
tmp[1] | 下标(位) | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
一个int值占32位(4*8)
那申请 int类型 tmp数组个数为:‘int tmp[1+N/32]’,N为int类型存储的最大值
1 | 比如要存储的最大值是50(0~50),那就是 1+(50/32) = 2.56 |
这样一来,如果给定一个M值,想知道在bit中的位置,可以这样算,M/32得到tmp数组下标,M%32得到数组中具体的下标位置。
1 | 比如,M值40,想知道这个M值,在bit中的位置,这样计算 |
tmp[0] | 0 | *** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | *** | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
tmp[0] | 31 | *** | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | *** | 3 | 2 | 1 | 0 |
tmp[1] | 0 | *** | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
tmp[1] | 63 | *** | 43 | 42 | 41 | 40 | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 |
2.1. BitMap 添加
如果想将数字5放入下面这个byte中,应该怎么做?
bit值 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
首先我们计算下5应该所属的位置
1 | 5 / 32 = 0.156,那就是在tmp[0]中 |
如下,两个表格’按位或’
one-byte tmp[0] | bit值 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
one-byte tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
two-byte tmp[0] | bit值 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|
two-byte tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
得到下面表格
new tmp[0] | bit值 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
new tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
换算成二进制形式
one-byte | two-byte = new-byte
one-byte | 0101 0110 |
---|---|
two-byte | 0010 0000 |
new-byte | 0111 0110 |
转换成十进制形式
one-byte | 2+4+16+64=86 |
---|---|
two-byte | 32 |
new-byte | 2+4+16+32+64=118 |
相当于
1 | 86 | 32 = 118 |
再简化下,得出公式
1 | p + (i/8)|(1<<(i%8)) |
也就是说,想要插入一个数,将1左移至代表该数字那一位的下标位置,然后与原数进行’按位或’操作。
2.2. BitMap 清除
假设我们要将6移除,该怎么操作?
下面两个表格,进行’按位与’操作。
one-byte tmp[0] | bit值 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
one-byte tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
two-byte tmp[0] | bit值 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|---|---|---|---|
two-byte tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
得到以下
new tmp[0] | bit值 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|
new tmp[0] | 下标(位) | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
可以看到,只需将下标为6的bit值,至为0即可。
也就是,1左移6位,到达代表这个数字的位,然后’按位取反’,然后与原数’按位与’,即可清除。
3. BitMap Demo
jdk中对BitMap数据结构进行了实现:java.util.BitSet
1 | public static void testBitMap() { |
3.1. BitSet的实现
在jdk的java.util.BitSet中,封装了BitMap的实现
BitSet底层用一个long型数组来存储元素。
1 | private final static int ADDRESS_BITS_PER_WORD = 6; |
使用long型数组存储元素,说明BitSet容量最小是8*8(64)bit,可以存储64个bit元素;而且BitSet的大小是long型(64)的整数倍。
3.1.1. BitSet构造函数
BitSet有三个构造方法,都会创建一个’位集合’,所有位默认都是false
空构造方法,初始化长度为1的long型数组
1 | public BitSet() { |
看下有参构造函数的源码
1 | private BitSet(long[] words) { |
long型参数的构造方法,入参即为words数组长度。
int型参数的构造方法,入参需要计算出words数组长度
举个栗子
1 | 传参为1,通过计算,构建的long型数组,words[]长度为1,因为只有一个long元素,size为64 |
是否指定words数组的大小
1 | /** |
在有参构造方法中,使用initWords方法,初始化数组长度;
initWords方法,会把nbits这个数减1然后右移6位再加1(wordIndex(nbits-1) + 1),如此计算出long型数组初始长度。
1 | private void initWords(int nbits) { |
例如
1 | initWords方法 |
BitSet用long类型,因为一个long占64bit,所以每个long表示64个元素。
也就是说,数组中words中的第一个long(words[0])表示0~63,第二个long(words[1])表示64~127,第三个long(words[2])表示128~191,依次类推…
wordIndex方法是很重要,它用来定位某个数在words数组中的位置。
3.1.2. ADDRESS_BITS_PER_WORD 为啥是6?
在 wordIndex() 方法中,计算value所属数组下标,为啥 ADDRESS_BITS_PER_WORD 是6呢?
1 | private static int wordIndex(int bitIndex) { |
上面提过,BitSet底层存储的容器是 long类型数组,一个long元素,占64bit;
还记得之前的公式吗?定位一个值,再哪个bit中时,需要先 M/32 得到数组下标,再 M%32 得到数组内下标;
因为是long型,所以,定位数组下标需要 M/64。
代码层面使用 >> ‘右移位’ 来代替除法(因为位运算效率比除法高)
long型占64bit,64刚好是2的6次幂,所以 ADDRESS_BITS_PER_WORD 是 6。
3.1.3. BitSet 添加
看下Demo
1 | public static void testBitMap() { |
BitSet.set()方法,有两个入参
bitIndex 即要存储的值;
value 是一个boolean值,为true则存储;为false则删除
1 | public void set(int bitIndex, boolean value) { |
wordIndex 方法,定位 值 应该存储在words数组哪个下标的long中,即wordIndex
1 | public void set(int bitIndex) { |
'words[wordIndex] |= (1L << bitIndex)'这一步,是计算值在数组内的位置。
1 | 这一步转换下,即 |
是不是和上面BitMap中的例子很像
运行上面的demo,在 words[wordIndex] |= (1L << bitIndex); 这一行,debug观察下
words下标 | 十进制 | 二进制 |
---|---|---|
0 | 2 | 10 |
0 | 6 | 110 |
0 | 14 | 1110 |
0 | 30 | 11110 |
0 | 62 | 111110 |
0 | 126 | 1111110 |
0 | 254 | 11111110 |
0 | 510 | 111111110 |
转换成二进制表格,即
bit值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标(位) | 63 | … | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
很明显吧,下标 1 ~ 8 的bit值,都是1,即我们存储的值
3.1.4. BitSet 查询
demo
1 | public static void testBitMap() { |
看下源码
1 | public boolean get(int bitIndex) { |
wordIndex方法上面讲过了,即 定位 bitIndex 在words数组中的位置。
这里重点是:(words[wordIndex] & (1L << bitIndex))
结合demo,拆分开看下
1 | bitIndex = 3; |
转换成表格,即
bit值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标(位) | 63 | … | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
bit值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标(位) | 63 | … | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
再看最后一步,将上面两步 ‘按位与’
1 | (words[wordIndex] & (1L << bitIndex)) |
转换成表格即:
bit值 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
下标(位) | 63 | … | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
最后判断下标3的bit值,是否等于0,以此判断3,是否存在。
4. BitSet 实例
有1千万个随机数,随机数的范围在1到1亿之间;现在要求写出一种算法,将1到1亿之间没有在随机数中的数求出来?
1 | /** |
4.1. 缺点
BitMap的JDK实现,有一个缺点,数据稀疏问题。
当数据量少,且跨度极大也就是稀疏的时候,原生的位图不太适合
例如,我只存储两个数字,1 和 9999999,它们需要的空间会非常大。
这里记录下数据稀疏解决方案:EWAHCompressedBitmap、Roaring BitMap 算法
还有一个数据碰撞的问题
进度问题,暂不深究