BitMap 基本思想
一个 int 在计算机中占 4 个字节。1 个字节占 8bit,也就是说一个 int 数字占 32bit。
而对于某些场景而言。这属于一种巨大的浪费,因为我们可以用对应的 32bit 位对应存储十进制的 0-31 个数,而这就是 BitMap 的基本思想。Bit-map 算法利用这种思想处理大量数据的排序、查询以及去重。
BitMap 代码实现
在讲代码之前我们先补充一些基础关于位运算的基础知识。基础还不错的同学可以跳过这部分。
int 在计算机中的存储方式
int t=1 那么 t 在计算机中如何存储的呢?
0000 0000 0000 0000 0000 0000 0000 0001
为什么要讲这个呢。因为在 BItMap 中涉及到关于 bit 的存储和计算。
运算符基础
左移 << :8 << 2 = > 8*4=32
8: 0000 0000 0000 0000 0000 0000 0000 1000
<<2: 0000 0000 0000 0000 0000 0000 0010 0000 => 2^5= 2*2*2*2*2=32
右移 >>:8 >> : 8 / 4 = 2
8: 0000 0000 0000 0000 0000 0000 0000 1000
>>2: 0000 0000 0000 0000 0000 0000 0000 0010 => 2^1=2
8 / 4 => 8 >> 2 8*4 => 8 << 2
- 位与 &:同位上的两个数都是 1 则位 1,否则为 0
- 位或 |:同为上的两个数只要有一个为 1 则为 1,否则为 0
OK,位运算的简单回顾就到这里,还有不懂的同学可以自行百度一下。
位图(BitMap)
通过以上知识我们可以知道 一个 int 占 32 个 bit 位。假如我们用这个 32 个 bit 位的每一位的值来表示一个数的话是不是就可以表示 32 个数字,也就是说 32 个数字只需要一个 int 所占的空间大小就可以了,瞬间就可以缩小空间 32 倍。
不过,位图有什么用呢?
有大用处哦,比如,我们要统计某个用户一年的活跃度,就可以使用位图来实现。
一年有 365 天,一个 long 类型可以表示 64 位,365/64=6,只需要 6 个 long 类型就可以记录一个用户一年的活跃情况,怎么记录呢?
很简单,初始时,位图中所有位都是 0,当这个用户某天登录了,就在位图中找到这天,把其位变成 1,一年下来,这张位图就记录了这个用户哪些天登录了,统计这个位图中 1 的数量,除以 365,就得到了他的活跃度。
BitMap 算法原理
假设我们要存储的数字是 64。那么我们要如何申请内存呢? 只要申请 64/32+1 个数组即可。即最大的数 64:bit[64/32+1]=bit[3];
bits[0]:0000 0000 0000 0000 0000 0000 0000 0000 0~31
bits[1]:0000 0000 0000 0000 0000 0000 0000 0000 32~63
bits[2]:0000 0000 0000 0000 0000 0000 0000 0000 64~95
-4/32=0 说明在 bit[0]中,下标 4%32=4 说明在第 5 个位置(下标从 0 开始)
- 65/32=2 说明在 bit[2]中,下标 65%32=1 说明在 bit[2]的第 2 个位置。
以下是代码实现
public class BitMap {
byte[] bits;
/**
* 初始化要存储的最大的那个数
*/
int max;
/**
* 初始化 bit 数组
*/
public BitMap(int max) {
this.max=max;
bits=new byte[(max>>3)+1];
}
/** 添加方法 */
public void add(int n){
// 向右移 3 位
int bitsIndex=n>>3;
// 取余
int loc=n%8;
// 将此下标的数组左移 loc 位置为 1
bits[bitsIndex]|=1<<loc;
}
public void delete(int n){
int bitsIndex=n>>3;
int loc=n%8;
// 先看是是否有该数。
if (find(n)){
// 将该位数字减一
bits[bitsIndex]-=1<<loc;
}
}
public Boolean find(int n){
int bitsIndex=n>>3;
int loc=n%8;
int flag=bits[bitsIndex]&(1<<loc);
return flag!=0;
}
public static void main(String[] args) {
BitMap bitMap=new BitMap(64);
bitMap.add(30);
bitMap.add(28);
bitMap.add(59);
bitMap.add(23);
bitMap.delete(30);
System.out.println(bitMap.find(28));
System.out.println(bitMap.find(30));
System.out.println(bitMap.find(40));
}
BitMap 应用之快速排序
假设我们要对 0-7 内的 5 个元素(4,7,2,5,3)排序(这里假设这些元素没有重复),我们就可以采用 Bit-map 的方法来达到排序的目的。要表示 8 个数,我们就只需要 8 个 Bit(1Bytes),首先我们开辟 1Byte 的空间,将这些空间的所有 Bit 位都置为 0,
0000 0000
对应位设置为 1:
0011 1101
遍历一遍 Bit 区域,将该位是一的位的编号输出(2,3,4,5,7),这样就达到了排序的目的,时间复杂度 O(n)。
- 优点: 运算效率高,不需要进行比较和移位;占用内存少,比如 N=10000000;只需占用内存为 N/8=1250000Byte=1.25M。
- 缺点:所有的数据不能重复。即不可对重复的数据进行排序和查找。
BitMap 应用之快速去重
2.5 亿个整数中找出不重复的整数的个数,内存空间不足以容纳这 2.5 亿个整数。
首先,根据“内存空间不足以容纳这 2.5 亿个整数”我们可以快速的联想到 Bit-map。下边关键的问题就是怎么设计我们的 Bit-map 来表示这 2.5 亿个数字的状态了。
其实这个问题很简单,一个数字的状态只有三种,分别为不存在,只有一个,有重复。
因此,我们只需要 2bits 就可以对一个数字的状态进行存储了,假设我们设定一个数字不存在为 00,存在一次 01,存在两次及其以上为 11。
那我们大概需要存储空间几十兆左右。 接下来的任务就是遍历一次这 2.5 亿个数字,如果对应的状态位为 00,则将其变为 01; 如果对应的状态位为 01,则将其变为 11;如果为 11,,对应的转态位保持不变。
最后,我们将状态位为 01 的进行统计,就得到了不重复的数字个数,时间复杂度为 O(n)。
BitMap 应用之快速查询
同样,我们利用 Bit-map 也可以进行快速查询,这种情况下对于一个数字只需要一个 bit 位就可以了,0 表示不存在,1 表示存在。假设上述的题目改为,如何快速判断一个数字是够存在于上述的 2.5 亿个数字集合中。 同之前一样,首先我们先对所有的数字进行一次遍历,然后将相应的转态位改为 1。遍历完以后就是查询,由于我们的 Bit-map 采取的是连续存储(整型数组形式,一个数组元素对应 32bits),我们实际上是采用了一种分桶的思想。一个数组元素可以存储 32 个状态位,那将待查询的数字除以 32,定位到对应的数组元素(桶),然后再求余(%32),就可以定位到相应的状态位。如果为 1,则代表改数字存在;否则,该数字不存在。就是我们代码中的 find 方法的实现,只不过我们在代码中是申请的 8 位数组。所以求余(%8)就可以了。