Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

BloomFilter布隆过滤器 简介

丈黑起恋秘
90 0 0
布隆过滤器
- T% f$ w2 B* C: A/ o布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
8 Y: r9 g; N+ `6 B+ e1 a, |布隆过滤器 (Bloom Filter)是一种space efficient的概率型数据结构,在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,false positive rate(误报率)越大,但是false negative (漏报)是不可能的。
9 r& ]7 l( x2 r5 U基本概念
4 o  U  m6 B# x2 E1 E如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为 O(n),O(log n),O(n/k)。
# Y0 j. |' P$ M( r1 L% @布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。9 Z7 K! S- u* C7 ?, Y1 O0 P7 ~
算法描述
8 D) i/ n9 r& P2 ?4 Z. M2 d一个empty bloom filter是一个有m bits的bit array,每一个bit位都初始化为0。并且定义有k个不同的hash function,每个都以uniform random distribution将元素hash到m个不同位置中的一个。在下面的介绍中n为元素数,m为布隆过滤器或哈希表的slot数,k为布隆过滤器重hash function数。
1 M3 o4 A  ~) ?% b为了add一个元素,用k个hash function将它hash得到bloom filter中k个bit位,将这k个bit位置1。
" I  D  M! G; m5 V为了query一个元素,即判断它是否在集合中,用k个hash function将它hash得到k个bit位。若这k bits全为1,则此元素在集合中;若其中任一位不为1,则此元素比不在集合中(因为如果在,则在add时已经把对应的k个bits位置为1)。7 }# n/ b- V. ^) P" x, y+ ]
不允许remove元素,因为那样的话会把相应的k个bits位置为0,而其中很有可能有其他元素对应的位。因此remove会引入false negative,这是绝对不被允许的。9 N6 w6 ?. G, |7 i3 ?4 E
当k很大时,设计k个独立的hash function是不现实并且困难的。对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份。或者可将k个不同的初始值(例如0,1,2, … ,k-1)结合元素,feed给一个hash function从而产生k个不同的数。7 i$ `# O3 d! x! Y$ o; p" B/ N" ^
当add的元素过多时,即n/m过大时(n是元素数,m是bloom filter的bits数),会导致false positive过高,此时就需要重新组建filter,但这种情况相对少见。
3 s7 ~2 y1 d" E3 S* I7 r优点- P  t' E( r- ~* k; P7 @
相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(O(k))。另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
) L7 G" w& U" n0 m* l布隆过滤器可以表示全集,其它任何数据结构都不能;" K3 O1 [0 F% X% c) |- g+ P7 l
缺点
6 n6 V. B: `. P; P$ t但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。- _- \  t! D; @* P( D) D6 Y
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面。这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。2 M& O. `/ P. H3 Y; u* q. ?
在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。4 Z5 T0 _0 W" F% N; l/ I7 @# j
举例说明布隆过滤器的空间优势
( C3 |: g" M* \9 E先来一个结论:对于一个有1%误报率和一个最优k值的布隆过滤器来说,无论元素的类型及大小,每个元素只需要9.6 bits来存储。这个优点一部分继承自array的紧凑性,一部分来源于它的概率性。如果你认为1%的误报率太高,那么对每个元素每增加4.8 bits,我们就可将误报率降低为原来的1/10。add和query的时间复杂度都为O(k),与集合中元素的多少无关,这是其他数据结构都不能完成的。k是hash函数的个数。" T3 g- I) V+ R2 @
举例: 现有1亿个email的黑名单,元素的数量(即email列表)为 108。若采用布隆过滤器,取k=8(k为hash函数个数)。因为n为1亿,所以总共需要8*108。又因为在保证误判率低(后面解释)且k和m选取合适时,空间利用率为50%(后面会解释),所以总空间为$ Z( Y9 S! u2 ?: O; J: ?# ~" a

  r0 @( u$ W6 g( e空间优势" e0 u0 w/ f* h" }7 B
所需空间比上述哈希结构或者数组小得多,并且误判率在万分之一以下。为什么可以这样算,可以看下面。
6 R$ t( x/ A  [# t6 y1 a误判概率的证明和计算/ r& H" A/ h0 P* _0 [. r
该过程的详细说明来自于这个文章http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html,为了看懂求导过程,需要复习数学知识。
: {% |+ C  H. v& m6 _对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为:/ A1 Q$ B7 o& E# ?0 G

, ^  |. o9 ~* `" ?5 {- G则k个hash function中没有一个对其置位的概率为:1 C. B! [  Y* r: }3 p  u( L% d

: a( o6 i$ U% K2 j2 ?8 y1 v* Z7 t如果插入了n个元素,但都未将其置位的概率为:
- m4 @1 G8 f' y" Z# @8 i' H. H: s- K' K' h* X
现在考虑query阶段,若对应某个待query元素的k bits全部置位为1,则可判定其在集合中。因此将某元素误判的概率为:
" j8 b5 L$ W, E) m7 |' \4 `/ N$ s1 M; `/ Z7 Q, ^. y/ u# E2 |
由于0 ^2 M8 f9 t& |8 E  y

3 z  _5 X3 k+ n; ]: X,并且1/m 当m很大时趋近于0,所以
( j/ G5 O. N8 o9 ~现在计算对于给定的m和n,k为何值时可以使得误判率最低。设误判率为k的函数为:
: O" Y) C! w1 [4 r7 [
( n: g- R5 f# k
6 M2 f4 g3 C- Y" N
4 C- P6 T6 n. v7 t则简化为
% ^/ b  G% K. }! e: q0 e9 _4 L6 S2 z! u; }+ T1 G5 J
因为等式右边的底数上是函数,指数上也是函数,没有方法求这样组合函数的导数,只能取对数之后,变成乘法。我们有两个函数相乘的求导方法,求导的几个方法可以看参考资料,有很好的视频说明。
. f/ J4 n/ b5 s0 m两边取对数得9 f9 J* `7 O* n* [5 x# E
  u6 z. e$ s# a, h3 ~; V
两边对k求导得,这边涉及到乘法求导,对数求导,幂函数求导:
* P. T7 ~3 f% w) C! _
6 C! P9 I- O9 Q( C$ z下面求最值,
! ^4 U9 b/ r' ?/ b1 g* m1 L1 X6 S/ [* u. y
红圈中的等式是把两边看成xln(x)这种形式得到的,和该函数的单调性相关。数学上能不能这么操作我还不太清楚。数学好的大神可以留言解释一下。8 v' \9 K" a! d
因此,即当5 {7 x9 A0 S3 z5 _4 t+ l7 J

2 R* D, J$ F- c  u5 H时误判率最低,此时误判率为:0 D* W8 `) k! ]
7 K6 U# Q1 N8 o, G
可以看出若要使得误判率≤1/2,则:
# i; ]; ]1 ]- l0 O0 ?1 _; d6 ~7 d! Q4 c1 K( j( n: f6 T3 H5 ~) p' \. U
这说明了若想保持某固定误判率不变,布隆过滤器的bit数m与被add的元素数n应该是线性同步增加的。
& M- V  t; `* x设计和应用布隆过滤器的方法/ u& a3 Y- X( y
应用时首先要先由用户决定要add的元素数n和希望的误差率P。这也是一个设计完整的布隆过滤器需要用户输入的仅有的两个参数,之后的所有参数将由系统计算,并由此建立布隆过滤器。
) {$ @( K& Z/ c系统首先要计算需要的内存大小m bits:$ c- U. f- z! [+ v! M9 g  z
; F' f. ?3 _2 m; x6 A+ ~: }% m; g
再由m,n得到hash function的个数:4 e8 b+ e& x5 F
0 z0 S- V* R  J
至此系统所需的参数已经备齐,接下来add n个元素至布隆过滤器中,再进行query。
7 z5 i% `. E" S1 o根据公式,当k最优时:
: l( h8 |4 Y0 j
" v* y# ~9 ?& X6 k  m因此可验证当P=1%时,存储每个元素需要9.6 bits:
# N& p+ i( Y, D, f- m
; ^* r: ]3 A$ B  Z: a而每当想将误判率降低为原来的1/10,则存储每个元素需要增加4.8 bits:( o: u* x, ^' h4 I% ~

* B2 O) P" U* R7 f! `这里需要特别注意的是,9.6 bits/element不仅包含了被置为1的k位,还把包含了没有被置为1的一些位数。此时的6 q& X* U& U5 n; q+ z5 C

( @% P% Y7 `" g$ _# ?才是每个元素对应的为1的bit位数。) k( b+ `& A& j% n
+ V; v0 w* R) B
从而使得P(error)最小时,我们注意到:
- K5 n; A9 k, b  d
. E5 w, Y+ t& y9 Q& h( x, ~6 p中的
' q: l- ~; D/ O% Z' m6 r% G' r6 u2 U,即
' U- S7 Z0 j; j3 T% B) S& n
# `! d9 P6 g3 j" i此概率为某bit位在插入n个元素后未被置位的概率。因此,想保持错误率低,布隆过滤器的空间使用率需为50%。
/ E8 L  ]7 B. n" }4 yNeo中的布隆过滤器; r, W, g/ M& y1 e
上面的内容大部分抄袭http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html,原作者写的太好了,我只是加上一些我的理解,方便数学不好的道友理解。下面我们看看Neo中的Bloom Filter。: q! f* X' @: o2 v+ D$ v- {# N
using System.Collections;, S! k  C9 |* Q5 F) C2 v9 I
using System.Linq;
6 i+ ^* ~3 T$ N- J1 H7 }namespace Neo.Cryptography
+ b' k2 n& ~! ~7 [( S{
+ t3 _4 _- M" ]! Q    public class BloomFilter! C' C% C: @/ R+ T( d
    {1 U* g% d/ q4 _
        private readonly uint[] seeds;- o2 u2 a, g6 Y% {
        private readonly BitArray bits;
% H+ T! Z6 e9 K        public int K => seeds.Length;
$ q- p$ r$ k# {" h        public int M => bits.Length;
) M0 S9 w7 _0 ?! x! c        public uint Tweak { get; private set; }
1 s6 |; U* A# B2 b# _        public BloomFilter(int m, int k, uint nTweak, byte[] elements = null)# n, k1 D& o" x1 h
        {* c4 e2 \/ f- U1 {' W: |
            this.seeds = Enumerable.Range(0, k).Select(p => (uint)p * 0xFBA4C795 + nTweak).ToArray();$ Q$ N! Q1 L6 D; i9 @
            this.bits = elements == null ? new BitArray(m) : new BitArray(elements);
7 [0 O) p2 T2 [/ T            this.bits.Length = m;
/ Q9 `" R3 o. F! Q6 Q            this.Tweak = nTweak;7 R* s' e  a/ C! D1 D% y& {; T! x
        }5 o; q% D7 `/ n2 |4 x
        public void Add(byte[] element)' |2 O% i  P7 i
        {# t3 r5 w  J0 ~1 y. I8 K
            foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s)))5 i, c1 w- t. Z; L+ X" W) w
                bits.Set((int)(i % (uint)bits.Length), true);
0 l4 E- i( ?) Z% ~6 @/ ?        }
9 `1 w# D7 M6 v& R4 {4 z1 A        public bool Check(byte[] element)/ G0 E, Z+ ]0 u) e- F
        {( b) L3 `8 T0 `) Q" y" ~* R1 f
            foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s)))( D: |9 S6 U+ L& f% F* X" S% m
                if (!bits.Get((int)(i % (uint)bits.Length)))
1 o( E5 y2 f+ O( }% V* h$ w( f                    return false;
$ `: B- ?8 [. Y7 x  m8 n            return true;1 {2 _9 v0 M( v7 _
        }
* f' l; ~" L6 A/ T& }, R2 F/ u        public void GetBits(byte[] newBits)$ c, E- ^  o. ]4 A: \, o* `3 Z  P
        {' Y7 r3 H0 [1 |. G  ]/ ]
            bits.CopyTo(newBits, 0);
7 w. ?: l% q; o. M# a        }
) `2 `! e7 w, f1 D) f, \3 p    }% R* ~6 d4 L" `
}  [. B8 C. @/ b4 X+ P
前面讲了这么多,代码竟然这么短,分析分析。/ ?5 w" m# k+ ?% r
构造函数传入了m(多少位),k(hash函数种类),这个和我们前面分析根据p(错误率),和n(要插入的元素)来构造的思路不一样。所以Neo的这个版本应该是一个简化版本,输入的数据n应该是有范围的,具体的范围我们后面运行整个区块链的时候在观察,现在不知道n的个数有多大。hash函数使用了Murmur32,然后传入不同的seed模拟不同的hash函数,这个是可以的。使用linq,函数式编程代码非常简洁,这也是C#的一个优势啊。add,check函数都很容易看懂,确实实现很简洁。  b2 n) I( s9 p& k' Y

3 Q6 V$ a3 D# w. b总结
  O+ c% y) `) TBloom Filter是牛逼的数据结构,因为有很多数学知识在里面,虽然代码不长,但是能看完这篇文章的人,会感受到代码之美。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

丈黑起恋秘 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    2