布隆过滤器-时空优化大师

/ Algorithm / 0 条评论 / 387浏览

Wikipedia

A Bloom filter is a space-efficient probabilistic data structure, conceived by Burton Howard Bloom in 1970, that is used to test whether an element is a member of a set. False positive matches are possible, but false negatives are not – in other words, a query returns either "possibly in set" or "definitely not in set". Elements can be added to the set, but not removed (though this can be addressed with a "counting" filter); the more elements that are added to the set, the larger the probability of false positives.

布隆过滤器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的(好羞愧呀,几十年了我才刚开始认真研究)。它实际上是由一个很长的二进制向量和一系列随机映射函数组成,布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率(假正例False positives,即Bloom Filter报告某一元素存在于某集合中,但是实际上该元素并不在集合中)和删除困难,但是没有识别错误的情形(即假反例False negatives,如果某个元素确实没有在该集合中,那么Bloom Filter 是不会报告该元素存在于集合中的,所以不会漏报)。

Bloom Filter 可以解决什么问题

在日常生活中,包括在设计计算机软件时,我们经常要判断一个元素是否在一个集合中。比如在字处理软件中,需要检查一个英语单词是否拼写正确(也就是要判断 它是否在已知的字典中);在 FBI,一个嫌疑人的名字是否已经在嫌疑名单上;在网络爬虫里,一个网址是否被访问过等等。最直接的方法就是将集合中全部的元素存在计算机中,遇到一个新元素时,将它和集合中的元素直接比较即可。一般来讲,计算机中的集合是用哈希表(hash table)来存储的。它的好处是快速准确,缺点是费存储空间。当集合比较小时,这个问题不显著,但是当集合巨大时,哈希表存储效率低的问题就显现出来 了。

不需要存储key,依然可以传入的key进行过滤,节省空间(key都不存..就是这么强,解决了我需要缓存千万级key在内存中的难题)

举例说明

比如说,一个像 Yahoo,Hotmail 和 Gmai 那样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spamer)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的 email 地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。 如果用哈希表,每存储一亿个email 地址, 就需要1.6GB 的内存(用哈希表实现的具体办法是将每一个email地址对应成一个八字节的信息指纹, 然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email 地址需要占用十六个字节。一亿个地址大约要 1.6GB, 即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB 的内存。除非是超级计算机,一般服务器是无法存储的。(该段引用自吴军老师的数学之美)

若采用布隆过滤器,取k=8。因为n为1亿,所以总共需要$8*{10}^{8}bits$被置位为1,又因为在保证误判率低且k和m选取合适时,空间利用率为50%(后面会解释),所以总空间为:$\frac {8*{10}^{8}bits}{0.5}=1.6*{10}^{9}bits=200MB$所需空间比上述哈希结构小得多,并且误判率在万分之一以下。

一个使用布隆过滤器防止过多访问磁盘的例子

Bloom filter used to speed up answers in a key-value storage system. Values are stored on a disk which has slow access times. Bloom filter decisions are much faster. However some unnecessary disk accesses are made when the filter reports a positive (in order to weed out the false positives). Overall answer speed is better with the Bloom filter than without the Bloom filter. Use of a Bloom filter for this purpose, however, does increase memory usage.

算法描述

Bloom Filter Algorithm description

An example of a Bloom filter, representing the set {x, y, z}. The colored arrows show the positions in the bit array that each set element is mapped to. The element w is not in the set {x, y, z}, because it hashes to one bit-array position containing 0. For this figure, m = 18 and k = 3.

  1. 首先需要k个hash函数,每个函数可以把key散列成为1个整数
  2. 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
  3. 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
  4. 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。

所以布隆过滤有以下几个特点:

时间和空间上的优势

当可以承受一些False positives时,布隆过滤器比其它表示集合的数据结构有着很大的空间优势。例如self-balancing binary search trees, tries, hash tables或者array, linked lists,它们中大多数至少都要存储元素本身,而Linked structures还需要为存储指针付出额外的代价。对于一个有1%误报率和一个最优k值的布隆过滤器来说,无论元素的类型及大小,每个元素只需要9.6 bits来存储。这个优点一部分继承自array的紧凑性,一部分来源于它的概率性。如果你认为1%的误报率太高,那么对每个元素每增加4.8 bits,我们就可将误报率降低为原来的1/10(怎么计算的下面有解答)。add和query的时间复杂度都为O(k),与集合中元素的多少无关,这是其他数据结构都不能完成的。

False positives和K、M、N计算

K:hash function数 M: 哈希表的slot数(bit array的大小) N: 元素数

假设 Hash 函数以等概率条件选择并设置 Bit Array 中的某一位,m 是该位数组的大小,k 是 Hash 函数的个数,那么位数组中某一特定的位在进行元素插入时的 Hash 操作中没有被置位的概率是:$1-{\frac {1}{m}}.$

那么在所有 k 次 Hash 操作后该位都没有被置 "1" 的概率是: $\left(1-{\frac {1}{m}}\right)^{k}.$

如果我们插入了 n 个元素,那么某一位仍然为 "0" 的概率是: $\left(1-{\frac {1}{m}}\right)^{kn};$

因而该位为 "1"的概率是: $1-\left(1-{\frac {1}{m}}\right)^{kn}.$

现在检测某一元素是否在该集合中。标明某个元素是否在集合中所需的 k 个位置都按照如上的方法设置为 "1",但是该方法可能会使算法错误的认为某一原本不在集合中的元素却被检测为在该集合中(False Positives),该概率由以下公式确定: $p(error)=\left(1-\left[1-{\frac {1}{m}}\right]^{kn}\right)^{k}\approx \left(1-e^{-kn/m}\right)^{k}.$

对公式$p(error)=\left(1-e^{-kn/m}\right)^{k}.$求导,可以得出当 ${\displaystyle k={\frac {m}{n}}\ln 2}\approx 0.7*\frac{m}{n}$时误判率最低,此时的$p(error)=(1-{\frac {1}{2}})^{k} = 2^{-k} \approx {0.6185}^{\frac {m}{n}}$

${\displaystyle p=\left(1-e^{-({\frac {m}{n}}\ln 2){\frac {n}{m}}}\right)^{{\frac {m}{n}}\ln 2}}$

$\ln p=-{\frac {m}{n}}\left(\ln 2\right)^{2}.$

${\displaystyle m=-{\frac {n\ln p}{(\ln 2)^{2}}}}$

${\displaystyle {\frac {m}{n}}=-{\frac {\log _{2}p}{\ln 2}}\approx -1.44{\log _{2}p}}$

${\displaystyle k=-{\frac {\ln p}{\ln 2}}=-{\log _{2}p}.}$

${\displaystyle k={\frac {m}{n}}\ln 2}$

在给定False Positives Rate下如何设定K和M的值

看图说话,在k为最优解的情况下,x轴是n个数,y轴是p(error),m大小对p的影响。

Probability of false positives

The false positive probability ${\displaystyle p}$ as a function of number of elements ${\displaystyle n}$ in the filter and the filter size ${\displaystyle m}$. An optimal number of hash functions ${\displaystyle k=(m/n)\ln 2}$ has been assumed

实战

这里是直接copy的crossoverJie/JCSprout中的程序,然后每个测试用例上用注释描述了一下。

程序执行过程中需要设置启动参数-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+HeapDumpOnOutOfMemoryError

package com.crossoverjie.algorithm;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.junit.Assert;
import org.junit.Test;

import java.util.HashSet;
import java.util.Set;

public class BloomFiltersTest {

    //写入1000w条数据
    private static int count = 10000000;

    //构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。
    //根据上面的计算方式选择出合适k和m值,时间复杂度和空间复杂度最优
    @Test
    public void guavaTest() {
        long star = System.currentTimeMillis();
        BloomFilter<Integer> filter = BloomFilter.create(
                Funnels.integerFunnel(),
                count,
                0.01);

        for (int i = 0; i < count; i++) {
            filter.put(i);
        }

        Assert.assertTrue(filter.mightContain(1));
        Assert.assertTrue(filter.mightContain(2));
        Assert.assertTrue(filter.mightContain(3));
        Assert.assertFalse(filter.mightContain(count));
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - star));
    }


    //使用最直接的集合判断,内存直接溢出
    @Test
    public void hashMapTest(){
        long star = System.currentTimeMillis();

        Set<Integer> hashset = new HashSet<>(10000000) ;
        for (int i = 0; i < 10000000; i++) {
            hashset.add(i) ;
        }
        Assert.assertTrue(hashset.contains(1));
        Assert.assertTrue(hashset.contains(2));
        Assert.assertTrue(hashset.contains(3));

        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - star));
    }


    //自定义的布隆过滤器,性能比Guava差点
    @Test
    public void bloomFilterTest(){
        long star = System.currentTimeMillis();
        BloomFilters bloomFilters = new BloomFilters(count) ;
        for (int i = 0; i < count; i++) {
            bloomFilters.add(i + "") ;
        }
        Assert.assertTrue(bloomFilters.check(1+""));
        Assert.assertTrue(bloomFilters.check(2+""));
        Assert.assertTrue(bloomFilters.check(3+""));
        Assert.assertTrue(bloomFilters.check(999999+""));
        Assert.assertFalse(bloomFilters.check(400230340+""));
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - star));
    }
}

package com.crossoverjie.algorithm;

/**
 * Function:布隆过滤demo
 *
 * @author crossoverJie
 *         Date: 2018/11/20 22:32
 * @since JDK 1.8
 */
public class BloomFilters {

    /**
     * 数组长度
     */
    private int arraySize;

    /**
     * 数组
     */
    private int[] array;

    public BloomFilters(int arraySize) {
        this.arraySize = arraySize;
        array = new int[arraySize];
    }

    /**
     * 写入数据
     * @param key
     */
    public void add(String key) {
        int first = hashcode_1(key);
        int second = hashcode_2(key);
        int third = hashcode_3(key);

        array[first % arraySize] = 1;
        array[second % arraySize] = 1;
        array[third % arraySize] = 1;

    }

    /**
     * 判断数据是否存在
     * @param key
     * @return
     */
    public boolean check(String key) {
        int first = hashcode_1(key);
        int second = hashcode_2(key);
        int third = hashcode_3(key);

        int firstIndex = array[first % arraySize];
        if (firstIndex == 0) {
            return false;
        }

        int secondIndex = array[second % arraySize];
        if (secondIndex == 0) {
            return false;
        }

        int thirdIndex = array[third % arraySize];
        if (thirdIndex == 0) {
            return false;
        }

        return true;

    }


    /**
     * hash 算法1
     * @param key
     * @return
     */
    private int hashcode_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);
    }

    /**
     * hash 算法2
     * @param data
     * @return
     */
    private int hashcode_2(String data) {
        final int p = 16777619;
        int hash = (int) 2166136261L;
        for (int i = 0; i < data.length(); i++) {
            hash = (hash ^ data.charAt(i)) * p;
        }
        hash += hash << 13;
        hash ^= hash >> 7;
        hash += hash << 3;
        hash ^= hash >> 17;
        hash += hash << 5;
        return Math.abs(hash);
    }

    /**
     *  hash 算法3
     * @param key
     * @return
     */
    private int hashcode_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) {
        BloomFilters bloomFilter = new BloomFilters(10000);
        long hashcode = bloomFilter.hashcode_1("1");
        long hashcode2 = bloomFilter.hashcode_1("2");
        System.out.println(hashcode);
        System.out.println(hashcode2);
        System.out.println("=========");
        long hashcode3 = bloomFilter.hashcode_2("1");
        long hashcode4 = bloomFilter.hashcode_2("100");
        System.out.println(hashcode3);
        System.out.println(hashcode4);
        System.out.println("=========");

        long hashcode5 = bloomFilter.hashcode_3("1");
        long hashcode6 = bloomFilter.hashcode_3("100");
        System.out.println(hashcode5);
        System.out.println(hashcode6);
        System.out.println("=========");


        bloomFilter.add("12345");
        bloomFilter.add("100");
        bloomFilter.add("1000");


        boolean check = bloomFilter.check("9000");
        System.out.println("check=" + check);

        boolean check1 = bloomFilter.check("12345");
        System.out.println("check1=" + check1);
    }
}

参考文献:

[1] 数学之美二十一:布隆过滤器(Bloom Filter)

[2] http://en.wikipedia.org/wiki/Bloom_filter

[3] https://www.cnblogs.com/liyulong1982/p/6013002.html

[3] http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html

[4] 如何判断一个元素在亿级数据中是否存在?