一、BitMap算法简介
Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value,而Key即是该元素。由于采用了Bit为单位来存储数据,可以很大力度的节省空间,常用于对大量整数做去重和查询操作。
二、场景描述
在20亿个随机整数中找出某个数m是否存在其中,并假设32位操作系统,4G内存。
1byte=8bit
1kb=1024byte
1mb=1024kb
1gb=1024mb
java中 int类型占用4个字节=4*8=32bit
如果将20亿个整数放入内存中,需要占用多少内存?
-
如果每个数字都以int类型存储
20亿整数占用内存=20亿*4byte/1024/1024/1024 约等于 7.45G -
如果使用bit-map的思想,按位存储,即一位就可以代表一个数字
20亿整数占用内存=20亿*1bit/8/1024/1024/1024=约等于0.233G
从上述结果来看,按位存储比按字节存储数字节约了(7.45/0.233-1约等于31倍空间),而且按字节存储根据内存4G的要求无法一次性在内存中进行处理。
三、BitMap算法原理
如何用一位来表示一个数呢?
众所周知,每一位的取值无非就是0和1,给每一位编号,那么用0表示数字存在,1表示数字不存在。
假如现在有一个字节也就是8位的空间,那么按照BitMap,可以表示8个数字,
按照上图所示,该字节的8位表示了整数数组[6,5,2,0]。那么java中int类型占用了4个字节,也就是32位,那么就可以最多表示32个数字。超过32位数字呢?那就使用两个以上int去表示。
假设我们要存储的数字最大值位N,则申请int temp[1+N/32]的数组空间,其中:
temp[0]可表示 0~31
temp[1]可表示 32-63
.....以此类推
给定任一整数M,M数字所在数组中的下标位置就应该是M/32,M数字所在的位就是M%32
添加整数M
总共两步
1.找到整数所在数组temp的下标
int index = M/32
2.将temp[index] 的第M%32位置1
temp[index]=temp[index] | (1<<(M%32))
清除
根据bitMap的原理可知,想要清除掉一个数字,那就是将对应的位置0
总共两步
1.找到整数所在数组temp的下标
int index = M/32
2.将temp[index] 的第M%32位置0
temp[index]=temp[index] &(~ (1<<(M%32)))
查找
根据每一位代表一个数字,1表示存在,0表示不存在,那么只需要判断整数对应位是否位1即可
总共两步
1.找到整数所在数组temp的下标
int index = M/32
2.将temp[index] 的第M%32位置0
temp[index] &(1<<(M%32) != 0?"存在":"不存在"
四、BitMap的用处
快速排序
在添加元素的时候,本身就达到了有序状态,只需要遍历一遍输出即可,时间复杂度O(N)快速去重
快速查找
根据公式计算即可
五、手写BitMap
public class BitMap {
private int[] temp;
private int size;
public BitMap(int size) {
this.size = size;
//初始化temp
//由于一个int占32位,可表示32个数字,那么如果想要存储size个数字,则需要
this.temp = new int[size >> 5 + 1];
}
/**
* 存储num
*
* @param num 需要存储的整数
*/
public void setBit(int num) {
if (num < 0 || num > size) {
throw new RuntimeException("");
}
int index = num >> 5;
int bitIndex = num % 32;
temp[index] |= (1 << bitIndex);
}
/**
* 查找num
*
* @param num 需要存储的整数
*/
public boolean getBit(int num) {
if (num < 0 || num > size) {
throw new RuntimeException("");
}
int index = num >> 5;
int bitIndex = num % 32;
return (temp[index] & (1 << bitIndex)) != 0;
}
/**
* 删除num
*
* @param num
*/
public void deleteBit(int num) {
if (num < 0 || num > size) {
throw new RuntimeException("");
}
int index = num >> 5;
int bitIndex = num % 32;
temp[index] &= (~(1 << bitIndex));
}
public static void main(String[] args) {
BitMap bitMap = new BitMap(1000);
bitMap.setBit(100);
bitMap.setBit(400);
System.out.println(bitMap.getBit(100));
bitMap.deleteBit(100);
System.out.println(bitMap.getBit(100));
}
}
运行结果
true
false
六、BitSet
BitSet就是实现了Bit-Map算法。BitSet位于java.util包下,从JDK1.0开始就已经有了。该类实现了一个按需增长的位向量。位集的每一个组件都有一个boolean类型的值。BitSet的每一位代表着一个非负整数。可以检查、设置、清除单个位。一个BitSet可以通过逻辑与、逻辑或、逻辑异或去修改另一个BitSet。默认情况下,所有位的标识都是false。
6.1BitSet基本API
设值
// 设置一个整数,将其对应的位设置为true
set(int bitIndex)
// 设置一个整数,将其对应的位设置为value对应的值
set(int bitIndex, boolean value)
// 将[formIndex,toIndex)每一整数对应位设置为true
set(int fromIndex, int toIndex)
// 将[formIndex,toIndex)每一整数对应位设置为value对应的值
set(int fromIndex, int toIndex, boolean value)
清除
// 设置一个整数,将其对应的位设置为false
clear(int bitIndex)
// 将[formIndex,toIndex)每一整数对应位设置为false
clear(int fromIndex, int toIndex)
//将所有位都设置位false
clear()
检查
// 返回该整数对应的位的值
boolean get(int bitIndex)
6.2 BitSet实现原理
BitSet有三种构造方法,我们直接来看无参构造器
private final static int ADDRESS_BITS_PER_WORD = 6;
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD;
public BitSet() {
initWords(BITS_PER_WORD);
sizeIsSticky = false;
}
private void initWords(int nbits) {
words = new long[wordIndex(nbits-1) + 1];
}
private static int wordIndex(int bitIndex) {
return bitIndex >> ADDRESS_BITS_PER_WORD;
}
可以看到BitSet是使用long数组存储。那么long类型占用8个字节,即64位,一个long类型可表示64个数字。默认设置BitSet可表示最大的位数为64位。与上述自己实现的基本类似。
再来看set方法
public void set(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
//获取整数所在数组中位置
int wordIndex = wordIndex(bitIndex);
// 确保当前位集容量,如果已经超出当前容量,则扩容
expandTo(wordIndex);
// 将整数对应的位设置为1
words[wordIndex] |= (1L << bitIndex); // Restores invariants
checkInvariants();
}
get方法
public boolean get(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
checkInvariants();
int wordIndex = wordIndex(bitIndex);
return (wordIndex < wordsInUse)
&& ((words[wordIndex] & (1L << bitIndex)) != 0);
}
clear方法
public void clear(int bitIndex) {
if (bitIndex < 0)
throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
int wordIndex = wordIndex(bitIndex);
if (wordIndex >= wordsInUse)
return;
words[wordIndex] &= ~(1L << bitIndex);
recalculateWordsInUse();
checkInvariants();
}
可以看到JDK中的BitSet实现原理与第三节中一样,采用Bit-Map思想,BitSet封装较多的API,可供开发者们随意使用。