1. 问题引入:
比较经典的问题是: 在只能够使用2G的内存中,如何完成以下操作:
①:对10亿个不重复的整数进行排序。
②:找出10亿个数字中重复的数字。
无论是排序还是找重复的数字都需要将这10亿个数字加入到内存中在去进行操作,很明显,题目给出的2G内存限制说明了在这样的场景下是不能够将所有数都加入到内存中的
1000000000* 4/(1024* 1024* 1024) = 3.725G
那么这时候就需要用到 BitMap结构了
2. BitMap是什么?
bitMap使用一个bit为0/1作为map的value来标记一个数字是否存在,而map的key值正是这个数字本身。
相比于一般的数据结构需要用4个byte去存储数值本身,相当于是节省了 4*8:1 = 32倍的内存空间
3. BitMap的实现:
bitMap不一定要用bit数组,可以使用 int,long等等的基本数据类型实现,因为其实质都是在bit位上存数据,用哪种类型只是决定了最终实现出来的BitMap的内置数组中单个元素存放数据的多少
例如:java中的BitSet使用Long数组
BitMap的实现当然少不了位运算,先来明确几个常见位运算,这是实现BitMap的基础:
any | 1 = 1
any & 1 = any
any | 0 = any
any & 0 = 0
set(bitIndex): 添加操作
1 .确定该数处于数组中的哪个元素的位上
int wordIndex = bitIndex >> 5;
因为我用的是int[]实现,所以这里右移 5 位(2^5 = 32)
2 .确定相对于该元素中的位置偏移
int bitPosition = bitIndex & ((1 << 5) - 1);
这里相当于是 bitIndex % (1<<5)的取模运算,因为当取模运算的除数是2的次幂,所以可以使用以下的位运算来计算,提升效率(对比HashMap的容量为什么总是2的幂次方的问题,HashMap求下标时也是使用 hash&(n-1))
tips: 位运算的优先级是低于+,-等等的,所以要加上括号,防止发生不可描述的错误
3 .将该位置1
bits[wordIndex] |= 1 << bitPosition;
相当于是将指定位置处的bit值置1,其他位置保持不变,也就是将以这个bitIndex为key的位置为1
tips: 这里是参考了网上的各位大佬的文章,取余 + 按位或,又对比了下BitSet的源码:
words[wordIndex] |= (1L << bitIndex);
没有取余操作,直接|,这两个一样吗?答案当然是一样的
举个栗子:
1 << 148 == 1<< 20
1L << 125 ==1L<< 61
即对于int和long型数据,直接左移其位数相当于是附带了对其的取模操作
- 使用int数组实现的BitMap代码如下:
public class BitMap {
private final static int DEFINE_BIT_SIZE = 5;
private final static int DEFINE_CAPACITY = 1 << DEFINE_BIT_SIZE;
private int[] bits;
private int capacity;
public BitMap() {
this.capacity = DEFINE_CAPACITY;
initBits(DEFINE_CAPACITY);
}
public BitMap(int capacity) {
this.capacity = capacity;
initBits(capacity);
}
private void initBits(int nbs) {
bits = new int[(nbs >> 5) + 1];
}
private void checkSize(int targetSize) {
int requireSize = targetSize + 1;
if (bits.length <= requireSize) {
int require = Math.max(bits.length * 2, requireSize);
bits = Arrays.copyOf(bits, require);
}
}
private int getBitPosition(int bitIndex) {
//注意: 移位运算的优先级要低于 +,-
return bitIndex & ((1 << DEFINE_BIT_SIZE) - 1);
}
/**
* 插入值
* @param bitIndex
*/
public void set(int bitIndex) {
int wordIndex = bitIndex >> DEFINE_BIT_SIZE;
checkSize(wordIndex);
int bitPosition = getBitPosition(bitIndex);
bits[wordIndex] |= 1 << bitPosition;
}
/**
* 是否存在值
* @param bitIndex
* @return
*/
public Boolean get(int bitIndex) {
int wordIndex = bitIndex >> DEFINE_BIT_SIZE;
int bitPosition = getBitPosition(bitIndex);
return (bits.length >= wordIndex) && (bits[wordIndex] & 1 << bitPosition) != 0;
}
/**
* 清除指定值
* @param bitIndex
* @return
*/
public Boolean clear(int bitIndex){
int wordIndex = bitIndex >> DEFINE_BIT_SIZE;
int bitPosition = getBitPosition(bitIndex);
bits[wordIndex] &= ~(1<<bitPosition);
return !get(bitIndex);
}
}
4 . BitMap的三种常见应用
-
1.快速排序
其实就是将待排序的数据通过Bit-map映射到相应的bit上,然后顺序遍历各个bit,就得到了数据的排序
Bit-map排序的优点:运算效率高,不需要进行比较和移位,占用内存少
注意:Bit-map不能够对重复数据进行排序
-
2.快速去重
例如:2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数
一个数字的状态有三种,分别为:不存在,只有一个,重复.
因此用2个bit来表示数字的状态:不存在为00,只有一个为01,重复为11.
然后遍历这2.5亿个整数,对其bit位状态进行改变,最后统计状态位为01的个数,即为不重复整数的个数 -
3.快速查询
例如:如何快速判断一个数字是够存在于上述的2.5亿个数字集合中
首先对所有的数字进行一次遍历,并将其对应的bit-map状态位置位1.
然后进行查询,即对一个指定的数先找到其位状态存储的数组或桶(bit-map基于分桶的连续存储,采用int[]实现的bit-map一个桶有32bits),然后再通过对32取余来得到在桶中的具体的偏移量,这样就定位到了该整数对应的bit位.
若该位为1,则存在;否则不存在。
5 . BitMap的扩展应用————Bloom Filter(布隆过滤器)
总结:使用Bit-map的思想,我们可以将存储空间进行压缩,而且可以对数字进行快速排序、去重和查询的操作。
Bloom Fliter是Bit-map思想的一种扩展,它可以在允许低错误率的场景下,大大地进行空间压缩,是一种拿错误率换取空间的数据结构
当一个元素加入布隆过滤器中的时候,会进行哪些操作:
- 使用布隆过滤器中的K个相互独立的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
- 根据得到的哈希值,在位数组中把对应下标的值置为 1
当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
- 对给定元素再次进行相同的哈希计算;
- 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率),因此:布隆过滤器可能会存在误判的情况
总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
Bloom Filter的应用: 常用于解决缓存穿透等场景。