数据算法:Bloom Filter

我们在一些体量亿级的网站或平台注册账号的时候,输入完用户名或账号回车可能会遇到提示:“用户名已存在”。系统是如何这么快速的判断出用户名存在与否的呢?这有很多种解决方案:

  1. 线性查找:时空复杂度都很高。
  2. 二分查找:首先需要将所有的用户名进行排序,对于亿级数据排序是个比较耗资源的事情。

另一种数据结构 Bloom Filter 也可以解决上面的问题,要理解 Bloom Filter,首先需要了解 Hash,关于 Hash 本文不做赘述。

1. 什么是 Bloom Filter?

Bloom Filter 是一种空间高效的概率型数据结构,它由 Burton Howard Bloom 在 1970 年提出,用来检验一个元素是否属于一个集合。有可能会得到假阳性(False Positive)匹配判断,但是不会得到假阴性错误。换言之,查询会返回:

  • 可能在集合中;
  • 肯定不在集合中。

Bloom Filter 的性质:

  1. 与标准哈希表不同,固定大小的 Bloom Filter 可以表示包含任意数量元素的集合。
  2. 给定一个大集合 S = x 1 , x 2 , ⋯   , x n S = {x_{1}, x_{2}, \cdots, x_{n}} S=x1,x2,,xn,基本上来说,Bloom Filter 近似于集合成员操作: x ∈ S x \in S xS x x x 是否属于 S S S)。
  3. Bloom Filter 可能存在假阳性错误,因为这只会带来一个额外的数据访问操作,而不会导致错误的答案。即,对于不存在集合中的某个元素 x x x,Bloom Filter 可能会返回这个元素 x x x 存在该集合中。
  4. Bloom Filter 不会有假阴性错误,因为这会导致错误的答案。换句话说,如果 x x x 在集合中,Bloom Filter 必然会指出该元素 x x x 确实存在于该集合中。
  5. 添加元素永远不会失败。但是,随着元素的添加,假阳性率也随之上升,直到 Bloom Filter 中的所有比特都设置为 1,此时所有查询都会得到一个阳性结果。
  6. 从 Bloom Filter 中删除元素是不可能的,因为通过清除 k k k 个哈希函数生成的索引上的位来删除单个元素,可能会删除其他一些元素。比如,从图 1 删除索引位为 (2, 5, 6) 的 bloom 的时候,同时会删除 filter,因为 filter 的索引位 5 被清除了。
    图1 删除元素

存在两种可能的错误:

  • 假阳性(False Positive Errors): x ∉ S x \notin S x/S,但答案是 x ∈ S x \in S xS
  • 假阴性(False Negative Errors): x ∈ S x \in S xS,但答案是 x ∉ S x \notin S x/S

2. Bloom Filter 的误判率

Bloom Filter 的一个不足之处就是存在假阳性错误,可能把不在集合中的元素错判成集合中的元素。

假阳性概率的计算过程:假定 Bloom Filter 有 m m m 比特,里面有 n n n 个元素,每个元素对应 k k k 个哈希函数,当然这 m m m 比特里面有些是 1,有些是 0。先计算某个比特为 0 的概率。假如,在这个 Bloom Filter 中插入一个元素,它的第一个哈希函数会把过滤器中的某个比特置成 1,因此,任何一个比特被置成 1 的概率是 1 m \frac{1}{m} m1,它是 0 的概率则是 1 − 1 m 1 - \frac{1}{m} 1m1

对于过滤器中特定的位置,如果这个元素的 k k k 个哈希函数都没有把它设置成 1,其概率是 ( 1 − 1 m ) k ( 1 - \frac{1}{m})^{k} (1m1)k。如果过滤器中插入第二个元素,某个特定的位置依然没有被设置成 1,其概率是 ( 1 − 1 m ) 2 k ( 1 - \frac{1}{m})^{2k} (1m1)2k。如果插入了 n n n 个元素还没把某个位置设置成 1,其概率是 ( 1 − 1 m ) n k ( 1 - \frac{1}{m} )^{nk} (1m1)nk。反过来,一个比他在插入了 n n n 个元素后,被设置成 1 的概率则是 1 − ( 1 − 1 m ) n k 1 - ( 1 - \frac{1}{m})^{nk} 1(1m1)nk

现在假定这 n n n 个元素都放到了 Bloom Filter 中了,又新来了一个不在集合中的元素,由于它的哈希函数都是随机的,因此,它的任意一个哈希函数正好命中某个值为 1 的比特的概率就是 1 − ( 1 − 1 m ) n k 1 - ( 1 - \frac{1}{m})^{nk} 1(1m1)nk。一个不在集合中的元素被误判成在集合中,需要所有的散列函数对应的比特值都是 1,其假阳性错误的概率是:
[ 1 − ( 1 − 1 m ) k n ] k [1 - (1 - \frac{1}{m})^{kn}]^{k} [1(1m1)kn]k

lim ⁡ m → ∞ ( 1 − 1 m ) m = 1 e \lim_{m \to \infty}(1 - \frac{1}{m})^{m} = \frac{1}{e} limm(1m1)m=e1,将 p = e − n m p = e^{-\frac{n}{m}} p=emn 代入得:
f ( k ) = [ 1 − ( 1 − 1 m ) k n ] k ≈ ( 1 − e − k n m ) k = ( 1 − p k ) k f(k) = [1 - (1 - \frac{1}{m})^{kn}]^{k} \approx (1 - e^{-\frac{kn}{m}})^{k} = (1 - p^{k})^{k} f(k)=[1(1m1)kn]k(1emkn)k=(1pk)k

对化简后的 l n f ( k ) = k l n ( 1 − p k ) lnf(k) = kln(1 - p^{k}) lnf(k)=kln(1pk) 中的 f ( k ) f(k) f(k) 求导得:
f ′ ( k ) = [ l n ( 1 − p k ) − p k l n p k 1 − p k ] ⋅ ( 1 − p k ) k {f}'(k) = [ln(1 - p^{k}) - \frac{p^{k}lnp^{k}}{1 - p^{k}}] \cdot (1 - p^{k})^{k} f(k)=[ln(1pk)1pkpklnpk](1pk)k

求最值,令 f ′ ( k ) = 0 {f}'(k) = 0 f(k)=0,由 ( 1 − p k ) k > 0 (1 - p^{k})^{k} > 0 (1pk)k>0 得:
( 1 − p k ) l n ( 1 − p k ) = p k l n p k (1 - p^{k})ln(1 - p^{k}) = p^{k}lnp^{k} (1pk)ln(1pk)=pklnpk

所以 p k = 1 2 p^{k}= \frac{1}{2} pk=21,将 p = e − n m p = e^{-\frac{n}{m}} p=emn 代入得到当哈希函数的数量 k k k 和 Bloom Filter 选择的位数 m m m、数据集的大小 n n n 满足下式时,假阳性错误的概率的概率最小。
k = m n l n 2 k = \frac{m}{n} ln2 k=nmln2

k = m n l n 2 k = \frac{m}{n} ln2 k=nmln2 代入 f ( k ) f(k) f(k) 得到:
p = f ( k ) = [ 1 − e − ( m n l n 2 ) n m ] m n l n 2 p = f(k) = [1 - e^{-(\frac{m}{n}ln2)\frac{n}{m}}]^{\frac{m}{n}ln2} p=f(k)=[1e(nmln2)mn]nmln2

化简可得:
m = n l n p ( l n 2 ) 2 m = \frac{nlnp}{(ln2)^{2}} m=(ln2)2nlnp

更详细的推导过程可以查看 Wikipedia 的 Bloom filter

3. Bloom Filter 示例

假设一个 Bloom Filter,有 10 比特( m = 10 m = 10 m=10)和三个散列函数 H 1 ( x ) , H 2 ( x ) , H 3 ( x ) H_{1}(x), H_{2}(x), H_{3}(x) H1(x),H2(x),H3(x),且有 H ( x ) = { H 1 ( x ) , H 2 ( x ) , H 3 ( x ) } H(x) = \left \{ H_{1}(x), H_{2}(x), H_{3}(x) \right \} H(x)={H1(x),H2(x),H3(x)}。如图 1 我们把一个 10 比特的数组 B B B 初始化为 0。
图2 bit 数组初始化
插入元素 bloom H ( b l o o m ) = ( 2 , 5 , 6 ) H(bloom) = (2, 5, 6) H(bloom)=(2,5,6) 后如图 3。
图3 插入元素 bloom
再插入元素 filter H ( f i l t e r ) = ( 1 , 5 , 8 ) H(filter) = (1, 5, 8) H(filter)=(1,5,8) 后如图 4。
图4 插入元素 filter
【实验 1】假设查询元素 test ,且 H ( t e s t ) = ( 5 , 8 , 9 ) H(test) = (5, 8, 9) H(test)=(5,8,9),则 Bloom Filter 判断 test 不是集合中的元素,因为 B [ 9 ] = 0 B[9] = 0 B[9]=0

【实验 2】假设查询元素 hello ,且 H ( h e l l o ) = ( 2 , 5 , 8 ) H(hello) = (2, 5, 8) H(hello)=(2,5,8),则 Bloom Filter 判断 hello 是集合中的元素,虽然 hello 确实不在集合中,但是 hello 的哈希后的索引上位都是 1。此时即为假阳性错误。

【实验 3】假设查询元素 world ,且 H ( w o r l d ) = ( 1 , 2 , 6 ) H(world) = (1, 2, 6) H(world)=(1,2,6),则 Bloom Filter 判断 world 是集合中的元素,同理虽然 world 确实不在集合中,但是 world 的哈希后的索引上位都是 1。此时亦为假阳性错误。

【实验 4】假设查询元素 bloom ,且 H ( b l o o m ) = ( 2 , 5 , 6 ) H(bloom) = (2, 5, 6) H(bloom)=(2,5,6),则 Bloom Filter 判断 bloom 是集合中的元素,因为 bloom 的哈希后的索引上位都是 1。

Bloom Filter 的简单实现:

public class BloomFilter {
    /* bit array的size */
    private int size;

    /* 哈希函数的个数 */
    private int hashCount;

    /* Bloom Filter的bit array */
    private BitSet bitArray;

    /* Bloom Filter的False Positive probability */
    private float falsePositiveProb;

    /**
     * @param itemCount 预期存储在Bloom Filter中的元素个数
     * @param falsePositiveProb Bloom Filter的False Positive probability
     */
    public BloomFilter(int itemCount, float falsePositiveProb) {
        this.size = this.computeSize(itemCount, falsePositiveProb);
        this.hashCount = this.computeHashCount(size, itemCount);
        this.falsePositiveProb = falsePositiveProb;
        this.bitArray = new BitSet(size);
    }

    /**
     * 用公式 m = -(n * ln(p)) / (ln(2)^2) 计算bit array的size
     * @param n 预期存储在Bloom Filter中的元素个数
     * @param p Bloom Filter的False Positive probability
     * @return bit array的size
     */
    private int computeSize(int n, float p) {
        double m = -(n * Math.log(p)) / (Math.pow(Math.log(2), 2));
        return (int) m;
    }

    /**
     * 用公式 k = (m/n) * ln(2) 计算哈希函数的个数
     * @param m bit array的size
     * @param n 预期存储在Bloom Filter中的元素个数
     * @return 哈希函数的个数
     */
    private int computeHashCount(int m, int n) {
        double k = (m/n) * Math.log(2);
        return (int) k;
    }

    public void add(Object item) {
        for (int i = 0; i < hashCount; i++) {
            int index = Math.abs(MurmurHash.hash(Objects.toString(item).getBytes(), i)) % size;
            bitArray.set(index);
        }
    }

    public boolean check(Object item) {
        for (int i = 0; i < hashCount; i++) {
            int index = Math.abs(MurmurHash.hash(Objects.toString(item).getBytes(), i)) % size;
            if (!bitArray.get(index)) {
                return false;
            }
        }

        return true;
    }

    public float getFalsePositiveProb() {
        return falsePositiveProb;
    }

    public int getSize() {
        return size;
    }

    public int getHashCount() {
        return hashCount;
    }
}

4. 总结

Bloom Filter 是一种空间高效的概率型数据结构,不需要存储元素。同时,Bloom Filter 的缺陷也比较明显,它存在假阳性错误的误判,而且一般情况下无法删除元素。

扫码关注公众号:冰山烈焰的黑板报
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值