布隆过滤器 Bloom Filter

布隆过滤器

布隆过滤器(Bloom Filter)是1970年由Burton Howard Bloom提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。

原理

如果想判断一个元素是不是在一个集合里,一般想到的是将所有元素保存起来,然后通过比较确定。链表,树等等数据结构都是这种思路. 但是随着集合中元素的增加,我们需要的存储空间越来越大,检索速度也越来越慢(O(n),O(logn))。

这时候就可以利用哈希表这个数据结构(它可以通过一个Hash函数将一个元素映射成一个位阵列(Bit array)中的一个点)。这样一来,我们只要看看这个点是不是1就知道可以集合中有没有它了。这就是Bloom Filter的基本思想。

但这时,哈希冲突会是一个问题:假设Hash函数是良好的,如果我们的位阵列长度为m个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳m/100个元素。显然这就不叫空间效率了(Space-efficient)了。解决方法也简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。如果它们都说在,虽然也有一定可能性它们都在说谎,不过直觉上判断这种事情的概率是比较低的。简单的说这种多个Hash组成的数据结构就叫Bloom Filter。

初始状态时,Bloom Filter是一个包含m位的位数组,每一位都置为0。

Bloom_Filter初始

为了表达S={X1, X2,…,Xn}这样一个n个元素的集合,Bloom Filter使用k个相互独立的哈希函数(Hash Function),它们分别将集合中的每个元素映射到{1,…,m}的范围中。对任意一个元素X,第i个哈希函数映射的位置Hi(X)就会被置为1(1≤i≤k)。注意,如果一个位置多次被置为1,那么只有第一次会起作用,后面几次将没有任何效果。在下图中,k=3,且有两个哈希函数选中同一个位置(从左边数第五位)。

Bloom Filter

在判断Y是否属于这个集合时,我们对Y应用k次哈希函数,如果所有Hi(Y)的位置都是1(1≤i≤k),那么我们就认为Y是集合中的元素,否则就认为Y不是集合中的元素。下图中Y1就不是集合中的元素。Y2或者属于这个集合,或者刚好是一个误判。

Bloom Filter

应用布隆过滤器时,只需要由用户决定要容纳的元素数n和希望的误判率p。然后通过以下公式:

Bloom Filter公式

计算出位数组的长度m,然后通过m,n计算出哈希函数的个数k。

Bloom Filter公式

布隆过滤器的优劣主要与哈希函数的质量相关,而且哈希函数之间的相关度越小越好,每个哈希函数本身的计算过程不要太复杂,不然会影响效率。理想情况下是取k个完全不相关的哈希函数,在不是很严格情况下,也可以通过一个哈希函数的参数变化产生k个不同的哈希函数,比如将i(1≤i≤k)作为参数参与哈希函数的计算。

应用场景

通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。例如邮件黑名单,爬虫url等处理。

优点

节约缓存空间(空值的映射),不再需要空值映射。
减少数据库或缓存的请求次数。
提升业务的处理效率以及业务隔离性。

缺点

存在误判的概率。常见的补救办法是在建立一个小的白名单。
传统的Bloom Filter不能作删除操作。

例子

import java.util.BitSet;

public class SimpleBloomFilter {

    // DEFAULT_SIZE为2的25次方
    private static final int   DEFAULT_SIZE = 2 << 24;
    /**
     * 不同哈希函数的种子,一般应取质数,seeds数据共有7个值,则代表采用7种不同的HASH算法
     */
    private static final int[] seeds        = new int[] { 5, 7, 11, 13, 31, 37, 61 };
    // BitSet实际是由“二进制位”构成的一个Vector。假如希望高效率地保存大量“开-关”信息,就应使用BitSet.
    // BitSet的最小长度是一个长整数(Long)的长度:64位
    private BitSet             bits         = new BitSet(DEFAULT_SIZE);
    /**
     * 哈希函数对象
     */
    private SimpleHash[]       func         = new SimpleHash[seeds.length];

    public static void main(String[] args) {
        String value = "stone2083@yahoo.cn";
        // 定义一个filter,定义的时候会调用构造函数,即初始化七个hash函数对象所需要的信息。
        SimpleBloomFilter filter = new SimpleBloomFilter();
        // 判断是否包含在里面。因为没有调用add方法,所以肯定是返回false
        System.out.println(filter.contains(value));
        filter.add(value);
        System.out.println(filter.contains(value));
    }

    // 构造函数
    public SimpleBloomFilter(){
        for (int i = 0; i < seeds.length; i++) {
            // 给出所有的hash值,共计seeds.length个hash值。共7位。
            // 通过调用SimpleHash.hash(),可以得到根据7种hash函数计算得出的hash值。
            // 传入DEFAULT_SIZE(最终字符串的长度),seeds[i](一个指定的质数)即可得到需要的那个hash值的位置。
            func[i] = new SimpleHash(DEFAULT_SIZE, seeds[i]);
        }
    }

    // 将字符串标记到bits中,即设置字符串的7个hash值函数为1
    public void add(String value) {
        for (SimpleHash f : func) {
            bits.set(f.hash(value), true);
        }
    }

    // 判断字符串是否已经被bits标记
    public boolean contains(String value) {
        // 确保传入的不是空值
        if (value == null) {
            return false;
        }
        boolean ret = true;
        // 计算7种hash算法下各自对应的hash值,并判断
        for (SimpleHash f : func) {
            // &&是boolen运算符,只要有一个为0,则为0。即需要所有的位都为1,才代表包含在里面。
            // f.hash(value)返回hash对应的位数值
            // bits.get函数返回bitset中对应position的值。即返回hash值是否为0或1。
            ret = ret && bits.get(f.hash(value));
        }
        return ret;
    }

    /* 哈希函数类 */
    public static class SimpleHash {

        // cap为DEFAULT_SIZE的值,即用于结果的最大的字符串长度。
        // seed为计算hash值的一个给定key,具体对应上面定义的seeds数组
        private int cap;
        private int seed;

        public SimpleHash(int cap, int seed){
            this.cap = cap;
            this.seed = seed;
        }

        // 计算hash值的具体算法,hash函数,采用简单的加权和hash
        public int hash(String value) {
            // int的范围最大是2的31次方减1,或超过值则用负数来表示
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                // 数字和字符串相加,字符串转换成为ASCII码
                result = seed * result + value.charAt(i);
                // System.out.println(result+"--"+seed+"*"+result+"+"+value.charAt(i));
            }
            // System.out.println("result="+result+";"+((cap - 1) & result));
            // System.out.println(414356308*61+'h'); 执行此运算结果为负数,为什么?
            // &是java中的位逻辑运算,用于过滤负数(负数与进算转换成反码进行)。
            return (cap - 1) & result;
        }
    }
}

  1. 暂无评论

  1. 暂无 Trackback

return top