数据结构和算法之十一:BitMap和布隆过滤器

数据结构之bitmap和布隆过滤器

bitmap

问题: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类,直接使用即可。

BloomFilter

bitmap好用是好用,但是它局限性也是很大的,就是它只能玩整数(或者可以转换成整数)的数据,解决不了这样的问题:

假如有一个1亿的黑名单email,如何来进行黑名单过滤垃圾邮件?

我们没办法直接用bitmap,此时就需要上我们神器,布隆过滤器来解决了。

布隆过滤器的原理很简单,底层还是bit数组,对进来的值通过多个hash函数,映射到bit数组的多个位上;查找时经过同样的hash映射,如果多个位都为1表示存在,如果有任意一个位为0,表示不存在。

图示一波

数据结构和算法之十一:BitMap和布隆过滤器_第1张图片

因为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,在解决一些看是大数据量的情况中有奇效,在资源有限的情况下能够解决部分大数据量的问题,关键是效率贼高,你说用不用嘛。

你可能感兴趣的:(数据结构与算法)