• 布隆过滤器原理与实现


    1. 布隆过滤器(Bloom filter)

    简单的说,布隆过滤器由一个长度为 m 的位数组和 k 个哈希函数组成。数组初始化为0。

    • 添加一个元素时,分别用 k 个哈希函数计算出该元素的哈希值,这 k 个哈希值作为数组的下标,并将数组中下标对应的值置为1;
    • 查询一个元素时,同样的,用 k 个哈希函数计算出该元素的哈希值,这 k 个哈希值作为数组的下标。我们去查询数组中对应下标的值,如果存在一个值为0,则表明被查询的元素一定不在集合中;如果对应下标的值都为1,则表明被查询的元素可能在集合中。如果一个元素其实并不存在,但被误认为存在,我们称其为“假阳性(false positive)

    下图展示了一个 m = 18, k = 3 的布隆过滤器。集合中的 x、y、z 三个元素通过 3 个不同的哈希函数散列到数组中。当查询元素 w 时,因为有一个比特为 0,因此判定 w 不在该集合中。

    img

    2. 假阳性概率分析(Probability of false positives)

    假设一个哈希函数以相等的概率选择每个数组的位置。在数组长度 m 的布隆过滤器中插入一个元素,它的其中一个哈希函数会将某个特定的比特置为 1。因此,在插入元素后,该比特仍然为 0 的概率是:

    1-{frac {1}{m}}.

    现在插 1 个元素,并假定有 k 个哈希函数,各个哈希值彼此不冲突,则该比特仍然为 0 的概率是:

    left(1-{frac {1}{m}}
ight)^{k}.

    假设布隆过滤器的数组长度 m 很大(m -> ∞),那么通过极限公式可推导出:

    {displaystyle left(1-{frac {1}{m}}
ight)^{k}=left(left(1-{frac {1}{m}}
ight)^{m}
ight)^{k/m}approx e^{-k/m}.}

    在插入 n 个元素后,该比特位上仍然保持为 0 的概率是:

    {displaystyle left(1-{frac {1}{m}}
ight)^{kn}approx e^{-kn/m};}

    因此,反过来说,该比特位为 1 的概率是:

    {displaystyle 1-left(1-{frac {1}{m}}
ight)^{kn}approx 1-e^{-kn/m}.}

    现在,我们用一个不在集合中的元素来检测,那么被误报为存在于集合中的概率(也就是所有哈希函数对应的比特都为 1 的概率),即假阳性概率为:

    {displaystyle varepsilon =left(1-left[1-{frac {1}{m}}
ight]^{kn}
ight)^{k}approx left(1-e^{-kn/m}
ight)^{k}.}

    上述的推导并不是完全正确的,因为推导背后假定每一位被设置的概率是独立的。然而事实上并不是独立的,因为误报率会随着数组长度 m 的增大而减小,随着插入元素个数 n 的增大而增大。

    更详尽的分析请参考wiki

    哈希函数个数 k 的确定

    另外,如果提高 k(即哈希函数的个数)就可以降低误报率。然而,如果 k 太大,那么计算哈希值的代价也会随之增大。为了使误报率最低,我们必须合理的选择 k 的大小。怎么确定 k 的大小呢?假定已经根据空间限制合理选出了 m 的大小,并且根据数据量确定了 n 的大小。也就是已知 m 和 n,可推导出 k :

    具体的推导过程如下(这部分非常重要,理解了才能真正明白为什么参数要这么设定,也才能读懂开源bloom filter的实现):

    3. 时间/空间复杂度

    布隆过滤器在时间和空间方面的表现都是非常高效的。假设布隆过滤器的位数组长度为 m,有 k 个哈希函数。

    3.1 时间

    添加和查询的时间复杂度均为 O(k),即对于每个元素都要经过 k 次计算得到哈希值(也就是数组的下标)。查看数组对应位置上的值只需要O(1)的时间。

    3.2 空间

    空间复杂度就是 O(m)

    4. 优缺点

    4.1 优点

    • 用比特数组表示,不用存储数据本身,对空间的节省相比于传统方式占据绝对的优势。
    • 时间效率很高,无论是插入还是查询,只需要简单的经过哈希函数转换,时间复杂度均为 O(k)。

    4.2 缺点

    • 存在false positives的情况,对于准确率要求高的场景不太适用。
    • 只能插入和查询,不能删除元素。
      • 一般的布隆过滤器不支持delete操作,但是,Counting filters provide a way to implement a delete operation on a Bloom filter without recreating the filter afresh. (备忘,暂时没有深入了解)

    5. 常见应用场景

    • 网页爬虫对URL的去重,避免爬取相同的URL地址;
    • 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
    • 海量数据判重;
    • 防止缓存穿透,将存在的数据放到布隆过滤器中,当访问不存在的数据时迅速返回,避免穿透进入数据库查询。
    • ...

    6. 实现

    完整代码可参考这里

    package bloomfilter
    
    import (
    	"fmt"
    	"github.com/spaolacci/murmur3"
    	"github.com/willf/bitset"
    	"math"
    )
    
    type BloomFilter struct {
    	m uint // size of bloom filter
    	k uint // number of hash functions
    	b *bitset.BitSet
    }
    
    func max(x, y uint) uint {
    	if x > y {
    		return x
    	} else {
    		return y
    	}
    }
    
    // 通过哈希函数产生4个基础哈希值
    func baseHashes(data []byte) [4]uint64 {
    	a1 := []byte{1} // to grab another bit of data
    	hasher := murmur3.New128()
    	hasher.Write(data) // #nosec
    	v1, v2 := hasher.Sum128()
    	hasher.Write(a1) // #nosec
    	v3, v4 := hasher.Sum128()
    	return [4]uint64{
    		v1, v2, v3, v4,
    	}
    }
    
    // location returns the ith hashed location using the four base hash values
    func location(h [4]uint64, i uint) uint64 {
    	ii := uint64(i)
    	return h[ii%2] + ii*h[2+(((ii+(ii%2))%4)/2)]
    }
    
    // 通过对哈希值求余,确保位置落在[0,m)范围内
    func (bf *BloomFilter) location(h [4]uint64, i uint) uint {
    	return uint(location(h, i) % uint64(bf.m))
    }
    
    // 根据元素个数n,错误率p,计算出合适的布隆过滤器的参数
    // 这里的 Log(2) 就是数学表示中的 ln(2)
    // 这里的公式计算推导参考博客,或者参考https://en.wikipedia.org/wiki/Bloom_filter
    func EstimateParameters(n uint, p float64) (m uint, k uint) {
    	m = uint(math.Ceil(-1 * float64(n) * math.Log(p) / math.Pow(math.Log(2), 2)))
    	k = uint(math.Ceil(math.Log(2) * float64(m) / float64(n)))
    	return
    }
    
    func New(m, k uint) *BloomFilter {
    	return &BloomFilter{
    		m: max(m, 1), // 这么做是为了避免错误输入,保证了m至少为正数
    		k: max(k, 1), // 同上
    		b: bitset.New(m),
    	}
    }
    
    func NewWithEstimates(n uint, p float64) *BloomFilter {
    	m, k := EstimateParameters(n, p)
    	return New(m, k)
    }
    
    // 对于每个插入的数据项,分别计算出k个哈希值,作为位数组的下标
    func (bf *BloomFilter) Add(data []byte) *BloomFilter {
    	h := baseHashes(data)
    	for i:=uint(0); i < bf.k; i++ {
    		bf.b.Set(bf.location(h, i)) //利用第i个哈希值确定对应的需要标记的位置
    	}
    	return bf
    }
    
    // Test returns true if the data is in the BloomFilter, false otherwise.
    // If true, the result might be a false positive. If false, the data
    // is definitely not in the set.
    func (bf *BloomFilter) Test(data []byte)  bool {
    	h := baseHashes(data)
    	for i := uint(0); i < bf.k; i++ {
    		if !bf.b.Test(bf.location(h,i)) { // 如果遇到某一位为0,则必定不存在
    			return false
    		}
    	}
    	return true
    }
    
    func main() {
    	//b := bitset.New(10)
    	//b.Set(4)
    	//fmt.Println(b.Test(4)) // true
    	//fmt.Println(b.Test(5)) // false
    	//fmt.Println(b.Test(100)) // false
    
    	filter := New(uint(1024), uint(3))
    	filter.Add([]byte("ZJU"))
    	fmt.Println(filter.Test([]byte("ZJU"))) // true
    	fmt.Println(filter.Test([]byte("ZJNU"))) // false
    }
    

    参考:

    1. https://en.wikipedia.org/wiki/Bloom_filter
    2. https://codeburst.io/lets-implement-a-bloom-filter-in-go-b2da8a4b849f
    3. https://brilliant.org/wiki/bloom-filter/
    4. https://juejin.im/post/5ea007476fb9a03c576cc7fd#heading-1
    5. https://blog.csdn.net/v_july_v/article/details/6685894
  • 相关阅读:
    项目经理手记
    自动填写版权信息
    在批处理中实现等待/延迟/暂停
    使用 PowerDesigner 设计数据库
    如何让PowerDesigner支持自动生成含SQL Server 2000的表和列注释的角本
    管理人员的招聘始末谈
    PowerDesign 6简易介绍
    PowerDesigner11.0使用总结
    用表单字段加亮的方式为用户提供友好的界面
    择才有道——企业招聘方式比较
  • 原文地址:https://www.cnblogs.com/kkbill/p/12771243.html
Copyright © 2020-2023  润新知