问题:100个数,取值范围在0-2亿,如何快速的判断一个数是否在这100个数中?
思路1:开一个数组,数值对应数组的下标,对存在的100个数进行标记,比如设置为1;这样,查找一个数是否存在时,直接通过数组下面获取对应的值,判断是否为1即可。
这个思路非常简单高效,就是有一个问题,因为取值范围0-2亿,开个两个亿的空间数组有点不大科学,而且如果内存有限制的话,还开不出来。
思路2:既然我只是对存在的数做一个标记,那么用一个二进制位就可以了吧,0表示不存在,1表示存在,那么一个int类型中,每个元素都可以表示32个数字,这样空间对比开int数组,直接缩小32倍;在判断一个数是否存在时,只需要找到这个数对一个这个位是0还是1就知道了。
这种解决方案用到的就是bitmap,底层其实就是位数组。
使用bitmap,其实主要的就是数值和下标的映射关系,比如申请一个int数组,一个int元素有32位长,那么size为10的int数组可以表示320个数。
其实对应到内存中,比如如下32个位,可表示0-31共32个数字。
0 0 0 0 0000 0000 0000 0000 0000 0000 0000
31,30,29,28…
对于int数组,数字n 的位置位于 int[n / 32]中的 n % 32 这个位上。
有了这些,我们就可以自己实现一个bitmap了(需要有位运算的知识):
/**
* Bitmap
*
* 用位(bit)来表示一个数(元素)是否存在,相对于用boolean或者其他类型来表示一个元素是否存在,会节约大量的空间。
*
* 比如有100个数,取值范围在0-2亿,如何快速的判断一个数是否在这100个数中?
*
* 思路
* 申请一个int数组,每一个数组元素有4个字节,也就是32个bit,用二进制来表示的话,如下
* 0000 0000 0000 0000 0000 0000 0000 0000
* 那么这样一个32个bit就可以表示32个数是否存在。
* 比如
* 0000 0000 0000 0000 0000 0000 0000 0000 表示0
* 0000 0000 0000 0000 0000 0000 0000 0010 表示1
* 0000 0000 0000 0000 0000 0000 0000 0100 表示2
* 0000 0000 0000 0000 0000 0000 0000 1000 表示3
* 以此类推,int[0]就可以表示0-31,int[1]就可以表示32-63 ....
* 那么int[n]就可以表示n*32 到(n+1)*32-1 的数。
* 回到前面的问题, 范围在0-2亿的数,我需要开辟的int数组大小为 2亿/32 + 1 。
*
* 映射关系, 数字n 的位置位于 int[n / 32]中的 n % 32 位上。
*
*/
public class BitMap {
private int max;
private int[] bits;
public BitMap(int max) {
this.max = max;
bits = new int[max / 32 + 1];
}
/**
* 加入
* @param n
*/
public void add(int n){
int index = n >> 5; //定位到哪个 32 位的段上。
int loc = n & (32 - 1) ; //定位32位段上的哪个位 n % 32 的另外一种写法
bits[index] |= 1 << loc; //将loc的那个位置为1, 其他的位不变。
}
/**
* 移除
* @param n
*/
public void remove(int n){
int index = n >> 5;
int loc = n & (32 - 1) ;
bits[index] &= (1 << loc) - 1; //将loc的那个位置为0, 其他的位不变。
}
public boolean isExist(int n){
int index = n >> 5;
int loc = n & (32 - 1) ;
return (bits[index] & (1 << loc)) != 0;
// return (bits[index] & (1 << loc)) >> loc == 1; //对应的位是否为1
}
public static void main(String[] args) {
BitMap bitMap = new BitMap(200000000); //最大的范围2亿。
bitMap.add(1);
bitMap.add(1000000);
bitMap.add(100000000);
System.out.println(bitMap.isExist(1));
System.out.println(bitMap.isExist(100000000));
System.out.println(bitMap.isExist(100000001));
bitMap.remove(100000000);
System.out.println(bitMap.isExist(100000000));
}
}
如果是java的同学,java已经封装好了BitSet类,直接使用即可。
bitmap好用是好用,但是它局限性也是很大的,就是它只能玩整数(或者可以转换成整数)的数据,解决不了这样的问题:
假如有一个1亿的黑名单email,如何来进行黑名单过滤垃圾邮件?
我们没办法直接用bitmap,此时就需要上我们神器,布隆过滤器来解决了。
布隆过滤器的原理很简单,底层还是bit数组,对进来的值通过多个hash函数,映射到bit数组的多个位上;查找时经过同样的hash映射,如果多个位都为1表示存在,如果有任意一个位为0,表示不存在。
图示一波
因为hash存在冲突,那么布隆过滤器告诉你某个值存在时,有可能是不存在的。
理解原理后,我们可以实现一个自己的布隆过滤器:
/**
* 布隆过滤器
*
* 在bitmap的基础之上,对同一个元素通过多个hash映射到多个bit上,以此来判断这个元素存不存在。
* 画图理解
*
* 告诉你不存在,那么一定是不存在,告诉你存在,那么也可能不存在。
*
* 为什么布隆过滤器不支持删除操作?
* 因为一个key映射到多个bit上时,这几个bit有可能已经被其他元素设置为1了,也就是hash冲突了。
* 如果删除这个key,那么它映射的几个位设置为0,那么就会误伤其他的也映射到这几个位置的元素,在判断这些元素时就会返回不存在。
*
* 如果一定要删除怎么办呢?
* 通常通过一个辅助的map或set来保存删除的key,如果删除的key到达一定量级,可以重建一下布隆过滤器,清空删除的数据结构。
*
* Guava库中已经提供了封装的BloomFilter,可以直接使用。
*
* */
public class BloomFilter {
private int size; //加入的元素个数
private BitSet bits; //就是bitmap,一个位数组。
public BloomFilter(int size) {
this.size = size;
this.bits = new BitSet(size);
}
public void add(String key){
int hash1 = hash_1(key);
int hash2 = hash_2(key);
int hash3 = hash_3(key);
bits.set(hash1 % size, true);
bits.set(hash2 % size, true);
bits.set(hash3 % size, true);
}
public boolean isExist(String key){
int hash1 = hash_1(key);
int hash2 = hash_2(key);
int hash3 = hash_3(key);
return bits.get(hash1 % size)
&& bits.get(hash2 % size)
&& bits.get(hash3 % size);
}
/**
* 这3个hash函数是网上摘抄的。
*/
public int hash_1(String key) {
int hash = 0;
int i;
for (i = 0; i < key.length(); ++i) {
hash = 33 * hash + key.charAt(i);
}
return Math.abs(hash);
}
public int hash_2(String key) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < key.length(); i++) {
hash = (hash ^ key.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return Math.abs(hash);
}
public int hash_3(String key) {
int hash, i;
for (hash = 0, i = 0; i < key.length(); ++i) {
hash += key.charAt(i);
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return Math.abs(hash) ;
}
public static void main(String[] args) {
// O(1000000000)
//8bit= 1byte
BloomFilter bloomFilter = new BloomFilter(Integer.MAX_VALUE); //21亿
System.out.println(bloomFilter.hash_1("1"));
System.out.println(bloomFilter.hash_2("1"));
System.out.println(bloomFilter.hash_3("1"));
bloomFilter.add("1111");
bloomFilter.add("1123");
bloomFilter.add("11323");
System.out.println(bloomFilter.isExist("1"));
System.out.println(bloomFilter.isExist("1123"));
}
}
当然,google的Guava库中已经提供了封装的BloomFilter,完全可以直接使用。
最后,小结一下,bitmap和BloomFilter,在解决一些看是大数据量的情况中有奇效,在资源有限的情况下能够解决部分大数据量的问题,关键是效率贼高,你说用不用嘛。