什么是Bitmap
所谓的BitMap就是用一个bit位来标记某个元素所对应的value,而key即是该元素,由于BitMap使用了bit位来存储数据,因此可以大大节省存储空间。
重要特性
1.存储空间小
由于Bitmap是通过bit来标识状态,数据将会高度压缩因此占用存储空间极小,存储变小也可以带来很多其它性能红利,如:内存、磁盘IO、网络带宽等。
如果用int类型表示一个整数,占用空间为(4byte = 32bit)。若用bitmap表示一个整数,占用空间为1bit。 内存节省了32倍。假设需要对10亿个整数进行排序(10亿个数据不重复,且范围在0 ~ 2^32)
如果用int类型表示需要占用空间:3.72G 左右
如果用bitmap类型表示需要占用空间:510MB 左右
如果bitmap加上对应的压缩算法roaringbitmap: 360MB 左右
2.计算速度快
由于BitMap是通过bit位来表示的,因此后面的计算都是基于位运算的,速度都会得到大幅度提升。
基本思想
我们用一个具体例子来说明Bitmap的原理。假设我们要对0-7内的8个元素中的(6,2,7,1)四个元素进行排序(这里假设元素没有重复)。我们可以使用Bitmap算法达到排序目的。
1.开辟1 Byte空间(要表示8个数,我们需要8个bit(1Byte)),将这些空间所有的bit都设置为0,如下图:
2.然后依次将四个元素根据Value的值将对应的bit位置设置为 1,最终8个bit位状态如下图:
3.现在我们遍历一次,把值为1的bit的位置输出(1,2,6,7),这样便达到了排序的目的。
从上面的例子我们可以看出,BitMap算法的思想还是比较简单的,关键的问题是如何确定10进制的数到2进制的映射图,接下来我们研究一下map映射。
注意:由于BitMap是用角标来记录对应的值,而角标是从0开始计数的,因此普通的BitMap只适用于非负数的排序或者去重操作。
MAP映射
BitMap中1bit代表一个数字,1个int = 4Bytes = 4*8bit = 32 bit,假设现有N个非负整数,取值范围[0,N-1],那么N个数需要N/32 int空间,其中:a[0]在内存中占32为可以对应十进制数0-31,依次类推如下图:
a[0]--------->[0]~[31] ->bit表示[0000000000000000000000000000000000000]
a[1]--------->[32]~[63] ->bit表示[0000000000000000000000000000000000000]
a[2]--------->[64]~[95] ->bit表示[0000000000000000000000000000000000000]
.
.
.
.
.
a[n]--------->[N-32]~[N-1]
说明:n = (N/32) - 1
注意:这里需要提醒一下,a数组的size是由待排序数中的最大值决定,而不是待排序数个数决定的。例如:待排序数组是[3,1,35],这个数组只有三个数字,但是在开辟BitMap空间时应该根据最大值35,因此需要开辟2个int空间,这个地方大家可以思考一下。自己在学习过程中,发现网络上多篇文章在这个地方都是错误的,因此特意提醒!
接下来介绍如何用位运算将十进制数转换为对应的bit位:
1.求十进制数在对应数组a中的下标
十进制数0-31,对应在数组a[0]中,32-63对应在数组a[1]中,64-95对应在数组a[2]中………,使用数学归纳分析得出结论:对于一个十进制数n,其在数组a中的下标为:a[n/32]
2.求出十进制数在对应数a[i]中bit位的下标
例如十进制数1在a[0]的bit位下标为1,十进制数31在a[0]的bit位下标为31,十进制数32在a[1]的bit位下标为0。十进制0-31就对应0-31,而32-63则对应也是0-31,即给定一个数n可以通过模32求得在对应数组a[i]中的bit位下标。
3.位移运算
对于一个十进制数n,对应在数组a[n/32][n%32]中,但数组a毕竟不是一个二维数组,我们通过移位操作实现将对应的bit位置 1。
位运算公式:a[n/32] |= 1 << (n % 32),即 a[n/32] = a[n/32] |(1 << (n % 32))
公式还有一个可以优化的地方,n % 32 等价于 n & 0x1F,解释:(n & 0x1F) 保留n的后五位 相当于 n % 32
至此,整个MAP映射过程结束。需要注意的是,本示例中底层使用的存储是int[],如果换做其它类型数组,如:long,则对应公式需要做相应调整。
代码实现
根据上面介绍的算法原理,代码实现如下:
package com.zhouj.endless.ds;
import java.util.ArrayList;
import java.util.List;
/**
* @author zhouj
* @date 2021-07-20 22:28
* @desc BitMap实现
*/
public class BitMap {
// 最大值
private static final int N = Integer.MAX_VALUE;
// [0,2147483647]
private int[] a = new int[N / 32 + 1];
/**
* 设置bit位为1
*
* @param n
*/
public void setBit(int n) {
//row = n / 32 求十进制数在数组a中的下标
int row = n >> 5;
//相当于 n % 32 求十进制数在数组a[i]中的下标
a[row] |= 1 << (n & 0x1F);
}
// 判断所在的bit为是否为1
public boolean exits(int n) {
int row = n >> 5;
return (a[row] & (1 << (n & 0x1F))) != 0;
}
/**
* 展示前row个数组
*
* @param row
*/
public void display(int row) {
System.out.println("BitMap位图展示前N组");
for (int i = 0; i < row; i++) {
List<Integer> list = new ArrayList<>();
int temp = a[i];
for (int j = 0; j < 32; j++) {
list.add(temp & 1);
temp >>= 1;
}
System.out.println("a[" + i + "]--------->" + 32 * i + "~" + (32 * (i + 1) - 1) + "->bit表示" + list);
}
}
/**
* 展示第N个数组
*
* @param n
*/
public void displayN(int n) {
System.out.println("BitMap位图展示第N组");
List<Integer> list = new ArrayList<>();
int temp = a[n - 1];
for (int j = 0; j < 32; j++) {
list.add(temp & 1);
temp >>= 1;
}
System.out.println("a[" + (n - 1) + "]--------->" + 32 * (n - 1) + "~" + (32 * n - 1) + "->bit表示" + list);
}
public static void main(String[] args) {
int num[] = {1, 5, 30, 31, 64, 56, 159, 120, 21, 17, 35, 45, Integer.MAX_VALUE};
BitMap map = new BitMap();
for (int i = 0; i < num.length; i++) {
map.setBit(num[i]);
}
map.display(6);
int temp = Integer.MAX_VALUE;
if (map.exits(temp)) {
System.out.println("temp:" + temp + " exists");
}
map.displayN(Integer.MAX_VALUE / 32 + 1);
}
}
运行结果如下:
本文给出的只是BitMap原来简单实现,在实际应用中有更复杂的情况。JDK中,是直接提供的BitMap集合的实现类的:java.util.BitSet,有兴趣的可以去研究一下。另外介于篇幅原因,针对BitMap的应用将在下一篇文章进行介绍和分析。
感谢阅读!