Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

BloomFilter布隆过滤器 简介

丈黑起恋秘
89 0 0
布隆过滤器- X7 V7 |6 c4 L
布隆过滤器(英语:Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。# c$ o8 T- ?! |2 A3 w
布隆过滤器 (Bloom Filter)是一种space efficient的概率型数据结构,在垃圾邮件过滤的黑白名单方法、爬虫(Crawler)的网址判重模块中等等经常被用到。哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的1/8或1/4的空间复杂度就能完成同样的问题。布隆过滤器可以插入元素,但不可以删除已有元素。其中的元素越多,false positive rate(误报率)越大,但是false negative (漏报)是不可能的。
! k% V1 d( d  N基本概念
$ @  a; ~( k( s' e如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。链表、树、散列表(又叫哈希表,Hash table)等等数据结构都是这种思路。但是随着集合中元素的增加,我们需要的存储空间越来越大。同时检索速度也越来越慢,上述三种结构的检索时间复杂度分别为 O(n),O(log n),O(n/k)。
! }. a/ E) V$ p. x7 d8 w' |! w, ]布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
* i. L8 S' v& X3 Z4 @, M算法描述
8 s! I6 X; f) w' T4 |- A% Z% U0 h+ Q一个empty bloom filter是一个有m bits的bit array,每一个bit位都初始化为0。并且定义有k个不同的hash function,每个都以uniform random distribution将元素hash到m个不同位置中的一个。在下面的介绍中n为元素数,m为布隆过滤器或哈希表的slot数,k为布隆过滤器重hash function数。
  N# c! u% G, P0 d* A9 x  w1 m9 u为了add一个元素,用k个hash function将它hash得到bloom filter中k个bit位,将这k个bit位置1。) r; T3 e" d  l, V3 q9 c0 a$ n+ g
为了query一个元素,即判断它是否在集合中,用k个hash function将它hash得到k个bit位。若这k bits全为1,则此元素在集合中;若其中任一位不为1,则此元素比不在集合中(因为如果在,则在add时已经把对应的k个bits位置为1)。
$ f' I- ?) Z) U) Q$ U不允许remove元素,因为那样的话会把相应的k个bits位置为0,而其中很有可能有其他元素对应的位。因此remove会引入false negative,这是绝对不被允许的。
5 W( z" J" C6 H5 j! A7 H当k很大时,设计k个独立的hash function是不现实并且困难的。对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份。或者可将k个不同的初始值(例如0,1,2, … ,k-1)结合元素,feed给一个hash function从而产生k个不同的数。
: }, ]) |# h0 R* t当add的元素过多时,即n/m过大时(n是元素数,m是bloom filter的bits数),会导致false positive过高,此时就需要重新组建filter,但这种情况相对少见。4 ^" v( u+ ~9 [+ Z  |- Q
优点
' _! F' q8 W5 n# d7 _相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数(O(k))。另外,散列函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。
/ G4 {" U, v' ?) Q  m1 \布隆过滤器可以表示全集,其它任何数据结构都不能;: k* j5 F* Q, Z3 G1 h
缺点
0 B1 s2 U. K( N/ N但是布隆过滤器的缺点和优点一样明显。误算率是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。' w" Z! z, i0 ?7 _7 ^/ o0 ?
另外,一般情况下不能从布隆过滤器中删除元素。我们很容易想到把位数组变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全地删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面。这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。
& ?* J( r9 u4 ?2 \1 H% J在降低误算率方面,有不少工作,使得出现了很多布隆过滤器的变种。. S. o7 n" V4 Z
举例说明布隆过滤器的空间优势
4 j$ M& G3 D# U8 V3 ~, C先来一个结论:对于一个有1%误报率和一个最优k值的布隆过滤器来说,无论元素的类型及大小,每个元素只需要9.6 bits来存储。这个优点一部分继承自array的紧凑性,一部分来源于它的概率性。如果你认为1%的误报率太高,那么对每个元素每增加4.8 bits,我们就可将误报率降低为原来的1/10。add和query的时间复杂度都为O(k),与集合中元素的多少无关,这是其他数据结构都不能完成的。k是hash函数的个数。* _* H* X2 O! c: L* @
举例: 现有1亿个email的黑名单,元素的数量(即email列表)为 108。若采用布隆过滤器,取k=8(k为hash函数个数)。因为n为1亿,所以总共需要8*108。又因为在保证误判率低(后面解释)且k和m选取合适时,空间利用率为50%(后面会解释),所以总空间为. V. ]! ?: K8 h8 H' E0 T
# V; h+ Z% H* [; x, |/ J) S
空间优势$ O$ X+ P. }  o4 Y  `! j+ p
所需空间比上述哈希结构或者数组小得多,并且误判率在万分之一以下。为什么可以这样算,可以看下面。# @( n5 w  c' p1 m
误判概率的证明和计算
6 j; q/ K0 q, n2 B4 k5 r该过程的详细说明来自于这个文章http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html,为了看懂求导过程,需要复习数学知识。
- o0 T, N3 a9 _' j对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为:# l  ^8 d: @) C! ~
! I5 Q/ d/ t. B: c: K7 @
则k个hash function中没有一个对其置位的概率为:. m" O, X' h9 O5 p, u, G9 e
  Z0 E7 T( O( d  Y, y9 p1 i
如果插入了n个元素,但都未将其置位的概率为:6 y; O# V4 p) y! f3 Y- z! y
6 V) j  D/ S# K4 j. E- w& D2 q
现在考虑query阶段,若对应某个待query元素的k bits全部置位为1,则可判定其在集合中。因此将某元素误判的概率为:
6 v/ {4 i) R% Y) t2 |4 k& A4 \! Y( Y* x
由于
. O0 V4 Z' W6 j" p" r  G2 _; Z5 ~' |# C2 U7 ~
,并且1/m 当m很大时趋近于0,所以
( x8 b& `- h& b现在计算对于给定的m和n,k为何值时可以使得误判率最低。设误判率为k的函数为:* E9 l, T1 f' z8 K5 @% l) t
: d/ M/ R( K: g  `( d+ S4 w, d
' C) A9 b# }* l% e
; B! W/ B0 e" }  @
则简化为
* G+ K' n& _6 J& I
# _2 ^! a4 {! u3 k因为等式右边的底数上是函数,指数上也是函数,没有方法求这样组合函数的导数,只能取对数之后,变成乘法。我们有两个函数相乘的求导方法,求导的几个方法可以看参考资料,有很好的视频说明。  y3 z0 L5 B/ g7 v+ Z% w: ]
两边取对数得
& n( V1 ?! j* w7 F9 Q! J7 h5 C
# J- L  h6 Q2 w+ t& |$ a) l两边对k求导得,这边涉及到乘法求导,对数求导,幂函数求导:
. B. [" y) X+ l. _, d: \
. K4 d+ E7 b$ u0 m下面求最值,3 O7 b* g) P% X) F: p, Y

% a0 U% e* ?4 n& [2 M  o. o红圈中的等式是把两边看成xln(x)这种形式得到的,和该函数的单调性相关。数学上能不能这么操作我还不太清楚。数学好的大神可以留言解释一下。
) I( m( y- H5 S( n. v8 l因此,即当8 K" n  c- E* i3 I
- I% z- ]3 z8 u
时误判率最低,此时误判率为:
3 t6 L- o* c' O& A. U: p7 y4 L& P% }! o5 L- R7 g+ A
可以看出若要使得误判率≤1/2,则:
" U/ z( W  `8 I8 e1 Q* H6 `  e. x: h! |+ d; R! _
这说明了若想保持某固定误判率不变,布隆过滤器的bit数m与被add的元素数n应该是线性同步增加的。- g( o' o. n0 N, a4 v
设计和应用布隆过滤器的方法% w" ^8 G1 L6 V) `: ]( V4 G
应用时首先要先由用户决定要add的元素数n和希望的误差率P。这也是一个设计完整的布隆过滤器需要用户输入的仅有的两个参数,之后的所有参数将由系统计算,并由此建立布隆过滤器。  f4 H/ ~4 i% Z2 ^+ S
系统首先要计算需要的内存大小m bits:
) W3 W' s+ o3 Z5 y3 M4 _! I
! h- d+ H7 D# R( e2 |再由m,n得到hash function的个数:9 J1 ]8 q2 Z* a/ {
! _0 @# {. ]' g" R9 |
至此系统所需的参数已经备齐,接下来add n个元素至布隆过滤器中,再进行query。" U: S) e9 s3 z' z7 e' ~
根据公式,当k最优时:
1 R7 x; T. `3 c8 `# j2 _
4 y. z* M) A9 ~0 F+ E因此可验证当P=1%时,存储每个元素需要9.6 bits:6 k+ l# ^: d/ H7 d& b

7 d! \! c, N+ K而每当想将误判率降低为原来的1/10,则存储每个元素需要增加4.8 bits:
' h: _& F' {7 Q% v: D4 a# m" [' c1 |. b0 @3 j0 e3 e2 q
这里需要特别注意的是,9.6 bits/element不仅包含了被置为1的k位,还把包含了没有被置为1的一些位数。此时的
! o- m/ G( j" c: v* V" ~1 }
. V7 B* z0 P( P, B" h5 M才是每个元素对应的为1的bit位数。
) Z; N2 }; S1 E6 C6 W5 E' n6 _# M- f+ o. o7 R3 r% R
从而使得P(error)最小时,我们注意到:
! u. s: ~& O+ d: ]
8 j" X/ X8 R  C8 y4 S/ `中的+ d% k. c- f. Z- {- s
,即
  @3 ^. P- U) F) `( B7 y3 Z- n7 U1 I, Y
此概率为某bit位在插入n个元素后未被置位的概率。因此,想保持错误率低,布隆过滤器的空间使用率需为50%。
- B' \0 i% r( qNeo中的布隆过滤器
" P5 a1 }* b$ ?5 W+ K上面的内容大部分抄袭http://www.cnblogs.com/allensun/archive/2011/02/16/1956532.html,原作者写的太好了,我只是加上一些我的理解,方便数学不好的道友理解。下面我们看看Neo中的Bloom Filter。8 w3 J5 v2 W( x2 C6 U" E, F
using System.Collections;
0 x# |5 @  f' j+ rusing System.Linq;6 y2 c/ e- A" j9 y: }! G
namespace Neo.Cryptography2 h7 k! h1 f+ @, U, r2 V# {
{
( {2 }9 n! P4 U. r    public class BloomFilter
9 A' o  s+ O1 f6 {+ o3 a    {3 w  q* T) J- t7 k
        private readonly uint[] seeds;
2 A8 e5 I  E  z3 M3 ?" h        private readonly BitArray bits;
6 E+ Z9 i( h$ ?) N  H% R9 w* U        public int K => seeds.Length;5 E- a) {  Y% o# Q
        public int M => bits.Length;
8 c! t, S- ~6 p/ h/ v        public uint Tweak { get; private set; }
3 H: \/ h8 x# u: C& x& B        public BloomFilter(int m, int k, uint nTweak, byte[] elements = null)) s  _5 A6 ^/ @  A
        {  v  `8 Y) S+ p' _6 a" {
            this.seeds = Enumerable.Range(0, k).Select(p => (uint)p * 0xFBA4C795 + nTweak).ToArray();
4 P; g' h; C6 g; n+ R- i            this.bits = elements == null ? new BitArray(m) : new BitArray(elements);# n. X& I, X& s
            this.bits.Length = m;
. w$ h& P/ g3 v: ^, w4 q1 R9 `            this.Tweak = nTweak;/ I: l5 S# l* k
        }
- S. W4 X" }- R1 t" m9 \        public void Add(byte[] element)) w" ~. N$ U3 A3 X' }, j& K4 D
        {
% v: b% f; e  T1 F            foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s)))
3 t* ~0 K( q4 W                bits.Set((int)(i % (uint)bits.Length), true);
+ C! z. j. A0 F7 [# Z9 k+ n. w9 r6 a        }8 ~- L# S- S$ p: q% S* P9 X
        public bool Check(byte[] element)
8 S: D* t. ~& _9 r) I, y        {0 k1 e6 G9 L1 w0 h
            foreach (uint i in seeds.AsParallel().Select(s => element.Murmur32(s))): n5 z' E/ S$ I  P: O- @! l5 w
                if (!bits.Get((int)(i % (uint)bits.Length)))
% s1 N3 N  H# |8 V% ?: i) ]- X" J: L                    return false;* u" V! T3 q0 E7 `# r, D
            return true;
5 w+ d& {% Z: E0 x/ o; ~" Q4 T        }
4 B$ u- T& p& a& `3 q# @5 c        public void GetBits(byte[] newBits)
( c" t7 r; ~- x- q! T2 Z3 e; H! i        {
" R7 e0 u% I% c            bits.CopyTo(newBits, 0);
. r7 k* S1 o6 z9 B) h0 n        }
! w0 ?2 D% ]  _+ Z: J/ T& E& E    }' |. j; _4 T/ e8 ?
}
( A- ?: m+ B* w: Q前面讲了这么多,代码竟然这么短,分析分析。4 _/ {" S( w/ p6 ~& R3 d
构造函数传入了m(多少位),k(hash函数种类),这个和我们前面分析根据p(错误率),和n(要插入的元素)来构造的思路不一样。所以Neo的这个版本应该是一个简化版本,输入的数据n应该是有范围的,具体的范围我们后面运行整个区块链的时候在观察,现在不知道n的个数有多大。hash函数使用了Murmur32,然后传入不同的seed模拟不同的hash函数,这个是可以的。使用linq,函数式编程代码非常简洁,这也是C#的一个优势啊。add,check函数都很容易看懂,确实实现很简洁。; j. g! `' {6 T! L* L0 A" E

2 m4 W  p* N; S- d. Y9 N: x总结/ r; ?- @- ~+ q7 M, _1 O' z1 y3 P
Bloom Filter是牛逼的数据结构,因为有很多数学知识在里面,虽然代码不长,但是能看完这篇文章的人,会感受到代码之美。
BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

成为第一个吐槽的人

丈黑起恋秘 小学生
  • 粉丝

    0

  • 关注

    0

  • 主题

    2