博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
cuckoofilter java_布隆过滤器(BloomFilter)的原理、实现和探究
阅读量:5106 次
发布时间:2019-06-13

本文共 11240 字,大约阅读时间需要 37 分钟。

有时候我们需要判断一个元素是否在一个集合中。比如,在字处理软件中,需要检查一个单词是否拼写正确(也就是要判断它是否在已知的字典里);在警察系统中,一个嫌疑人的名字是否出现在嫌疑名单上;在网络爬虫里,一个网址是否已经被访问过,等等。

最直接的方法就是讲集合中的元素存在计算机中,遇到一个新元素时,将它和集合中的元素直接比较即可。一般来讲,计算机中的集合是用哈希表(Hash Table)来存储的。它的好处是快速准确,缺点是耗费存储空间。

为什么说耗费存储空间呢?其根本原因是哈希表方法需要把实实在在的具有特定长度(每个Email地址对应成一个8字节的信息指纹)的元素的信息指纹存储在内存或硬盘中的哈希表中,这个存储量在实际应用中一般是相当大的。比如每存储一亿个Email地址,需要0.8G大小的数字指纹存储空间,考虑到哈希表的存储空间利用率一般只有一半,所以需要1.6G的存储空间。如果存储几十亿上百亿的Email地址,那就需要百亿字节的内存存储空间。

而布隆过滤器只需要哈希表1/8到1/4的大小就能解决同样的问题,它实际上是一个很长的二进制向量和一系列的随机映射函数。

下面以WEB页面地址的存储为例来说明布隆过滤器的工作原理。

假定存储一亿个WEB页面地址,先建立一个16亿二进制(比特),即2亿字节的向量,然后将这16亿个二进制位清零。对于每一个WEB页面地址X,用8个随机数产生器(f1,f2,...,f8)。再用一个随机数产生器G把这8个信息指纹映射到1-16亿中的8个自然数g1,g2,...g8。现在把这8个位置的二进制位都置为1。对着一亿个WEB页面地址都进行这样的处理后,一个针对WEB页面的布隆过滤器就建成了,见下图。

80dbfe4391456459dca0efce761b3e3e.png

图1 布隆迪过滤器的映射方法

现在,让我们看看如何用布隆过滤器来检测一个WEB网页地址Y是否已经被我们收录。用相同的8个随机数生成器(f1,f2,...,f8)对这个WEB网页地址产生8个信息指纹s1,s2,...s8,然后将这8个指纹对应到布隆过滤器的8个二进制位,分别是t1,t2,...,t8。如果Y已被收录,显然t1,t2,...,t8对应的8个二进制位一定是1。通过这样的方式我们能够很快地确定一个WEB页面是否已被我们收录。

一. 算法描述

一个empty bloom filter是一个有m bits的bit array,每一个bit位都初始化为0。并且定义有k个不同的hash function,每个都以uniform random distribution将元素hash到m个不同位置中的一个。在下面的介绍中n为元素数,m为布隆过滤器或哈希表的slot数,k为布隆过滤器重hash function数。

为了add一个元素,用k个hash function将它hash得到bloom filter中k个bit位,将这k个bit位置1。

为了query一个元素,即判断它是否在集合中,用k个hash function将它hash得到k个bit位。若这k bits全为1,则此元素在集合中;若其中任一位不为1,则此元素比不在集合中(因为如果在,则在add时已经把对应的k个bits位置为1)。

不允许remove元素,因为那样的话会把相应的k个bits位置为0,而其中很有可能有其他元素对应的位。因此remove会引入false negative,这是绝对不被允许的。

当k很大时,设计k个独立的hash function是不现实并且困难的。对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份。或者可将k个不同的初始值(例如0,1,2, … ,k-1)结合元素,feed给一个hash function从而产生k个不同的数。

当add的元素过多时,即n/m过大时(n是元素数,m是bloom filter的bits数),会导致false positive过高,此时就需要重新组建filter,但这种情况相对少见。

二. 时间和空间上的优势

当可以承受一些误报时,布隆过滤器比其它表示集合的数据结构有着很大的空间优势。例如self-balance BST, tries, hash table或者array, chain,它们中大多数至少都要存储元素本身,对于小整数需要少量的bits,对于字符串则需要任意多的bits(tries是个例外,因为对于有相同prefixes的元素可以共享存储空间);而chain结构还需要为存储指针付出额外的代价。对于一个有1%误报率和一个最优k值的布隆过滤器来说,无论元素的类型及大小,每个元素只需要9.6 bits来存储。这个优点一部分继承自array的紧凑性,一部分来源于它的概率性。如果你认为1%的误报率太高,那么对每个元素每增加4.8 bits,我们就可将误报率降低为原来的1/10。add和query的时间复杂度都为O(k),与集合中元素的多少无关,这是其他数据结构都不能完成的。

如果可能元素范围不是很大,并且大多数都在集合中,则使用确定性的bit array远远胜过使用布隆过滤器。因为bit array对于每个可能的元素空间上只需要1 bit,add和query的时间复杂度只有O(1)。注意到这样一个哈希表(bit array)只有在忽略collision并且只存储元素是否在其中的二进制信息时,才会获得空间和时间上的优势,而在此情况下,它就有效地称为了k=1的布隆过滤器。

而当考虑到collision时,对于有m个slot的bit array或者其他哈希表(即k=1的布隆过滤器),如果想要保证1%的误判率,则这个bit array只能存储m/100个元素,因而有大量的空间被浪费,同时也会使得空间复杂度急剧上升,这显然不是space efficient的。解决的方法很简单,使用k>1的布隆过滤器,即k个hash function将每个元素改为对应于k个bits,因为误判度会降低很多,并且如果参数k和m选取得好,一半的m可被置为为1,这充分说明了布隆过滤器的space efficient性。

三. 举例说明

以垃圾邮件过滤中黑白名单为例:现有1亿个email的黑名单,每个都拥有8 bytes的指纹信息,则可能的元素范围为  4605ce63b2a448370e337c9da8829377.png ,对于bit array来说是根本不可能的范围,而且元素的数量(即email列表)为 cdf2a93bee96fd8a2df88c56f7bd4610.png ,相比于元素范围过于稀疏,而且还没有考虑到哈希表中的collision问题。

若采用哈希表,由于大多数采用open addressing来解决collision,而此时的search时间复杂度为 :

baf6cd3e79114f23f68e986a381ddf81.png

即若哈希表半满(n/m = 1/2),则每次search需要probe 2次,因此在保证效率的情况下哈希表的存储效率最好不超过50%。此时每个元素占8 bytes,总空间为:

fdc96a9367299e221a77914c93137a00.png

若采用Perfect hashing(这里可以采用Perfect hashing是因为主要操作是search/query,而并不是add和remove),虽然保证worst-case也只有一次probe,但是空间利用率更低,一般情况下为50%,worst-case时有不到一半的概率为25%。

若采用布隆过滤器,取k=8。因为n为1亿,所以总共需要 b30aa45ea2b3d88f6d85396591eb6418.png 被置位为1,又因为在保证误判率低且k和m选取合适时,空间利用率为50%(后面会解释),所以总空间为:

286bf2280afd9a51c2c1d4f1db7f059f.png

所需空间比上述哈希结构小得多,并且误判率在万分之一以下。

四. 误判概率的证明和计算

假设布隆过滤器中的hash function满足simple uniform hashing假设:每个元素都等概率地hash到m个slot中的任何一个,与其它元素被hash到哪个slot无关。若m为bit数,则对某一特定bit位在一个元素由某特定hash function插入时没有被置位为1的概率为:

2d04c61c0ea38746f836d660d3c272cb.png

则k个hash function中没有一个对其置位的概率为:

f526ba1c07796483736095a5f136a5cd.png

如果插入了n个元素,但都未将其置位的概率为:

6f76c0d1033653114f2aafb47e2303d3.png

则此位被置位的概率为:

1fad9179c7163e86c0beed7a9cf4c154.png

现在考虑query阶段,若对应某个待query元素的k bits全部置位为1,则可判定其在集合中。因此将某元素误判的概率为:

c6f0c1b00b545d84fef803a3374ac3aa.png

由于 186eda3a091d37e3c16c40253e2a789b.png,并且 748404a35b1d4f079f5d4c2a927b2fe0.png  当m很大时趋近于0,所以

c6e9be53cf44aa605d991421b0662722.png

从上式中可以看出,当m增大或n减小时,都会使得误判率减小,这也符合直觉。

现在计算对于给定的m和n,k为何值时可以使得误判率最低。设误判率为k的函数为:

da62fb0db4b7c7c280c3e07417ae3b72.png

设  a64648cf289588e2958976df5e03543d.png , 则简化为

48b0981bb448845ffe588d18e858be3a.png,两边取对数

6ea7d17a08e486e2286f8606dd56b15e.png  , 两边对k求导

e68ac8e5d2aa21fc43fb9d8eba3b65cf.png

下面求最值

5a6a6636059df3095445504b6a8a2ee4.png

6cf2e4daa823a627060bad1fa01da683.png 9a183374f1d6c11207af1a45b570afc7.png

faf4460d731125175743f9bd96e500a1.png 3c102f1ea422479715e1f7f23d76c102.png

921b08a3a2375dd812f57836925375fc.png a2e734d9e3eb3d04f80cd3f4d7288133.png

ebe554640c048bbc65eea58ec8f9933b.png de955133d11b3f14ce1a184fc24b2df3.png

7f0fead9fea324f522d8d75ba50b4120.png 13a22cf9f66e8f2b2a84e81d572d0192.png

455b7fce87840c92641e1a573e528b64.png 0238e0b0a93e9e744e56dd6171979df4.png

d2242bf52196883d1cfdf87e3546aac9.png d7bf4b6b83d55187b60e15aa19db0831.png

因此,即当 27740232684a74ee82e8a5aa3b0e5dbd.png  时误判率最低,此时误判率为:

71bce9916e7ab84ee73d53bb75c5a4d0.png

可以看出若要使得误判率≤1/2,则:

06617098c5d4fc997f6618d975e64a26.png

这说明了若想保持某固定误判率不变,布隆过滤器的bit数m与被add的元素数n应该是线性同步增加的。

五. 设计和应用布隆过滤器的方法

应用时首先要先由用户决定要add的元素数n和希望的误差率P。这也是一个设计完整的布隆过滤器需要用户输入的仅有的两个参数,之后的所有参数将由系统计算,并由此建立布隆过滤器。

系统首先要计算需要的内存大小m bits:

915ed02288a2f76bee8ffcda34686f7f.png

再由m,n得到hash function的个数:

cbcb5b20f7e07349911bbe18279db0cb.png

至此系统所需的参数已经备齐,接下来add n个元素至布隆过滤器中,再进行query。

根据公式,当k最优时:

04983dea1585ca13b83c243feec43a9a.png

00a8db4a4b8216b52cd68bad3e3fa075.png

因此可验证当P=1%时,存储每个元素需要9.6 bits:

5f896f8f2f07f36c797f6b285b788caf.png

而每当想将误判率降低为原来的1/10,则存储每个元素需要增加4.8 bits:

37699756d68a6f93325b946a71954b18.png

这里需要特别注意的是,9.6 bits/element不仅包含了被置为1的k位,还把包含了没有被置为1的一些位数。此时的

37e206b830392425ce51229b78ed45dc.png

才是每个元素对应的为1的bit位数。

ed1ecfc78e137621615b2e01e3dc7284.png   从而使得P(error)最小时,我们注意到:

3ce3731dc0e670d404305d14e0821b31.png 中的 2d5972bbcb038aacd5f3c96fb080f4a0.png  ,即

c2a8ec97fa970f583ac9ff48dbb09654.png

此概率为某bit位在插入n个元素后未被置位的概率。因此,想保持错误率低,布隆过滤器的空间使用率需为50%。

对于 BloomFilter 的概念和原理,以及误报率等计算方法都一个理性的认识了。在这里,我们将用 Java'实现一个简单的 BloomFilter 。

package pri.xiaoye.day1029;

import java.io.Serializable;

import java.nio.charset.Charset;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.BitSet;

import java.util.Collection;

/**

* 布隆过滤器的实现类

* 定义:http://en.wikipedia.org/wiki/Bloom_filter

*

* @param

* E指定了要插入过滤器中的元素的类型,eg,String Integer

* @author Magnus Skjegstad

* @translator xiaoye

*/

public class BloomFilter implements Serializable {

private static final long serialVersionUID = -2326638072608273135L;

private BitSet bitset;

private int bitSetSize;

private double bitsPerElement;

private int expectedNumberOfFilterElements;//能够添加的元素的最大个数

private int numberOfAddedElements;//过滤器容器中元素的实际数量

private int k; // 哈希函数的个数

static final Charset charset = Charset.forName("UTF-8");//存储哈希值的字符串的编码方式

static final String hashName = "MD5"; //在大多数情况下,MD5提供了较好的散列精确度。如有必要,可以换成 SHA1算法

static final MessageDigest digestFunction;//MessageDigest类用于为应用程序提供信息摘要算法的功能,如 MD5 或 SHA 算法

static { // 初始化 MessageDigest 的摘要算法对象

MessageDigest tmp;

try {

tmp = java.security.MessageDigest.getInstance(hashName);

} catch (NoSuchAlgorithmException e) {

tmp = null;

}

digestFunction = tmp;

}

/**

* 构造一个空的布隆过滤器. 过滤器的长度为c*n

* @param c

* 表示每个元素占有多少位

* @param n

* 表示过滤器能添加的最大元素数量

* @param k

* 表示需要使用的哈希函数的个数

*/

public BloomFilter(double c, int n, int k) {

this.expectedNumberOfFilterElements = n;

this.k = k;

this.bitsPerElement = c;

this.bitSetSize = (int) Math.ceil(c * n);

numberOfAddedElements = 0;

this.bitset = new BitSet(bitSetSize);

}

/**

* 构造一个空的布隆过滤器。最优哈希函数的个数将由过滤器的总大小和期望元素个数来确定。

*

* @param bitSetSize

* 指定了过滤器的总大小

* @param expectedNumberOElements

* 指定了过滤器能添加的最大的元素数量

*/

public BloomFilter(int bitSetSize, int expectedNumberOElements) {

this(bitSetSize / (double) expectedNumberOElements, expectedNumberOElements, (int) Math.round((bitSetSize / (double) expectedNumberOElements)* Math.log(2.0)));

}

/**

* 通过指定误报率来构造一个过滤器。

* 每个元素所占的位数和哈希函数的数量会根据误报率来得出。

*

* @param falsePositiveProbability

* 所期望误报率.

* @param expectedNumberOfElements

* 要添加的元素的数量

*/

public BloomFilter(double falsePositiveProbability, int expectedNumberOfElements) {

this(Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2)))/ Math.log(2), // c = k/ln(2)

expectedNumberOfElements,

(int) Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2)))); // k = ln(2)m/n

}

/**

* 根据旧过滤器的数据,重新构造一个新的过滤器

*

* @param bitSetSize

* 指定了过滤器所需位的大小

* @param expectedNumberOfFilterElements

* 指定了过滤器所能添加的元素的最大数量

* to contain.

* @param actualNumberOfFilterElements

* 指定了原来过滤器的数据的数量

* filterData BitSet.

* @param filterData

* 原有过滤器中的BitSet对象

*/

public BloomFilter(int bitSetSize, int expectedNumberOfFilterElements,

int actualNumberOfFilterElements, BitSet filterData) {

this(bitSetSize, expectedNumberOfFilterElements);

this.bitset = filterData;

this.numberOfAddedElements = actualNumberOfFilterElements;

}

/**

* 根据字符串的内容生成摘要

*

* @param val

* 字符串的内容

* @param charset

* 输入数据的编码方式

* @return 输出为一个long类型

*/

public static long createHash(String val, Charset charset) {

return createHash(val.getBytes(charset));

}

/**

* 根据字符串内容生成摘要

*

* @param val

* 指定了输入的字符串。默认的编码为 UTF-8

* @return 输出为一个long类型

*/

public static long createHash(String val) {

return createHash(val, charset);

}

/**

* 根据字节数组生成摘要

*

* @param data

* 输入数据

* @return 输出为long类型的摘要

*/

public static long createHash(byte[] data) {

long h = 0;

byte[] res;

synchronized (digestFunction) {

res = digestFunction.digest(data);

}

for (int i = 0; i < 4; i++) {

h <<= 8;

h |= ((int) res[i]) & 0xFF;

}

return h;

}

/**

* 重写equals方法

*/

@Override

public boolean equals(Object obj) {

if (obj == null) {

return false;

}

if (getClass() != obj.getClass()) {

return false;

}

final BloomFilter other = (BloomFilter) obj;

if (this.expectedNumberOfFilterElements != other.expectedNumberOfFilterElements) {

return false;

}

if (this.k != other.k) {

return false;

}

if (this.bitSetSize != other.bitSetSize) {

return false;

}

if (this.bitset != other.bitset

&& (this.bitset == null || !this.bitset.equals(other.bitset))) {

return false;

}

return true;

}

/**

* 重写了hashCode方法

*

*/

@Override

public int hashCode() {

int hash = 7;

hash = 61 * hash + (this.bitset != null ? this.bitset.hashCode() : 0);

hash = 61 * hash + this.expectedNumberOfFilterElements;

hash = 61 * hash + this.bitSetSize;

hash = 61 * hash + this.k;

return hash;

}

/**

* 根据最大元素数量和过滤器的大小来计算误报率。

* 方法的返回值为误报率。如果插入的元素个数小于最大值,则误报率会比返回值要小。

*

* @return 期望的误报率.

*/

public double expectedFalsePositiveProbability() {

return getFalsePositiveProbability(expectedNumberOfFilterElements);

}

/**

* 通过插入的元素数量和过滤器容器大小来计算实际的误报率。

*

* @param numberOfElements

* 插入的元素的个数.

* @return 误报率.

*/

public double getFalsePositiveProbability(double numberOfElements) {

// (1 - e^(-k * n / m)) ^ k

return Math.pow((1 - Math.exp(-k * (double) numberOfElements

/ (double) bitSetSize)), k);

}

/**

* 通过实际插入的元素数量和过滤器容器大小来计算实际的误报率。

*

* @return 误报率.

*/

public double getFalsePositiveProbability() {

return getFalsePositiveProbability(numberOfAddedElements);

}

/**

* 返回哈希函数的个数 k

*

* @return k.

*/

public int getK() {

return k;

}

/**

* 清空过滤器元素

*/

public void clear() {

bitset.clear();

numberOfAddedElements = 0;

}

/**

* 向过滤器中添加元素。

* 添加的元素的toString()方法将会被调用,返回的字符串作为哈希函数的输出。

*

* @param element

* 要添加的元素

*/

public void add(E element) {

long hash;

String valString = element.toString();

for (int x = 0; x < k; x++) {

hash = createHash(valString + Integer.toString(x));

hash = hash % (long) bitSetSize;

bitset.set(Math.abs((int) hash), true);

}

numberOfAddedElements++;

}

/**

* 添加一个元素集合到过滤器中

*

* @param c

* 元素集合.

*/

public void addAll(Collection extends E> c) {

for (E element : c)

add(element);

}

/**

* 用来判断元素是否在过滤器中。如果已存在,返回 true。

*

* @param element

* 要检查的元素.

* @return 如果估计该元素已存在,则返回true

*/

public boolean contains(E element) {

long hash;

String valString = element.toString();

for (int x = 0; x < k; x++) {

hash = createHash(valString + Integer.toString(x));

hash = hash % (long) bitSetSize;

if (!bitset.get(Math.abs((int) hash)))

return false;

}

return true;

}

/**

* 判断一个集合中的元素是否都在过滤器中。

*

* @param c

* 要检查的元素集合

* @return 如果集合所有的元素都在过滤器中,则返回true。

*/

public boolean containsAll(Collection extends E> c) {

for (E element : c)

if (!contains(element))

return false;

return true;

}

/**

* 得到某一位的值

*

* @param bit

* bit的位置.

* @return 如果该位被设置,则返回true。

*/

public boolean getBit(int bit) {

return bitset.get(bit);

}

/**

* 设置过滤器某一位的值

*

* @param bit

* 要设置的位置.

* @param value

* true表示已经成功设置。false表示改为被清除。

*/

public void setBit(int bit, boolean value) {

bitset.set(bit, value);

}

/**

* 返回存放信息的位数组.

*

* @return 位数组.

*/

public BitSet getBitSet() {

return bitset;

}

/**

* 得到过滤器中位数组个大小。

*

* @return 数组大小.

*/

public int size() {

return this.bitSetSize;

}

/**

* 返回已添加的元素的个数

*

* @return 元素个数.

*/

public int count() {

return this.numberOfAddedElements;

}

/**

* 得到能添加的元素的最大数量

*

* @return 最大数量.

*/

public int getExpectedNumberOfElements() {

return expectedNumberOfFilterElements;

}

/**

* 得到每个元素占用的位的个数的期望值

*

* @return 每个元素占用的位数

*/

public double getExpectedBitsPerElement() {

return this.bitsPerElement;

}

/**

* 得到每个元素占用位数的实际值

*

* @return 每个元素占用的位数.

*/

public double getBitsPerElement() {

return this.bitSetSize / (double) numberOfAddedElements;

}

}

转载地址:http://fqudv.baihongyu.com/

你可能感兴趣的文章
WCF揭秘——使用AJAX+WCF服务进行页面开发
查看>>
C#综合揭秘——细说多线程(下)
查看>>
c#运算符 ?
查看>>
ps互补色
查看>>
Silverlight学习笔记(九)-----RenderTransform特效【五种基本变换】及【矩阵变换MatrixTransform】...
查看>>
【题解】青蛙的约会
查看>>
【eclipse】点Clean后没反应
查看>>
springboot下html的js中使用shiro标签功能
查看>>
求给定字符串的最长子字符串
查看>>
.26-浅析webpack源码之事件流make(1)
查看>>
IO流
查看>>
mybatis调用存储过程,获取返回的游标
查看>>
Android Handler学习笔记
查看>>
设计模式之装饰模式(结构型)
查看>>
面向对象的设计原则
查看>>
解释性语言和编译性语言的区别
查看>>
Swift3.0服务端开发(三) Mustache页面模板与日志记录
查看>>
Java读取.properties配置文件的几种方法
查看>>
【转】 FPGA设计的四种常用思想与技巧
查看>>
移动端页面头部定义
查看>>