在对大量数据进行比较的时候,经常使用Hash,也听说过Bloom Filter,不过对后者没有了解过,直到在读吴军博士的《数学之美》时,正巧里面有一节内容就是介绍Bloom Filter,写的非常易懂,摘录如下。

在日常生活中,包括在设计计算机软件时,我们经常要判断一个元素是否在一个集合中。比如在字处理软件中,需要检查一个英语单词是否拼写正确(也就是要判断它是否在已知的字典中);在FBI,一个嫌疑人的名字是否已经在嫌疑名单上;在网络爬虫里,一个网址是否被访问过等等。最直接的方法就是将集合中全部的元素存在计算机中,遇到一个新元素时,将它和集合中的元素直接比较即可。一般来讲,计算机中的集合是用哈希表(hash table)来存储的。它的好处是快速准确,缺点是费存储空间。当集合比较小时,这个问题不显著,但是当集合巨大时,哈希表存储效率低的问题就显现出来了。比如说,一个象Yahoo,Hotmail和Gmai那样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spamer)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的email地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。如果用哈希表,每存储一亿个email地址, 就需要1.6GB的内存(用哈希表实现的具体办法是将每一个email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有50%,因此一个email地址需要占用十六个字节。一亿个地址大约要1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百GB的内存。除非是超级计算机,一般服务器是无法存储的。

今天,我们介绍一种称作布隆过滤器的数学工具,它只需要哈希表1/8到1/4的大小就能解决同样的问题。

布隆过滤器是由巴顿.布隆于一九七零年提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。我们通过上面的例子来说明起工作原理。

假定我们存储一亿个电子邮件地址,我们先建立一个十六亿二进制(比特),即两亿字节的向量,然后将这十六亿个二进制全部设置为零。对于每一个电子邮件地址X,我们用八个不同的随机数产生器(F1,F2, …,F8)产生八个信息指纹(f1, f2, …, f8)。再用一个随机数产生器G把这八个信息指纹映射到1到十六亿中的八个自然数g1, g2, …,g8。现在我们把这八个位置的二进制全部设置为一。当我们对这一亿个email地址都进行这样的处理后。一个针对这些email地址的布隆过滤器就建成了。(见下图)

bloom-filter

现在,让我们看看如何用布隆过滤器来检测一个可疑的电子邮件地址Y是否在黑名单中。我们用相同的八个随机数产生器(F1, F2, …, F8)对这个地址产生八个信息指纹s1,s2,…,s8,然后将这八个指纹对应到布隆过滤器的八个二进制位,分别是t1,t2,…,t8。如果Y在黑名单中,显然,t1,t2,..,t8 对应的八个二进制一定是一。这样在遇到任何在黑名单中的电子邮件地址,我们都能准确地发现。

布隆过滤器决不会漏掉任何一个在黑名单中的可疑地址。但是,它有一条不足之处。也就是它有极小的可能将一个不在黑名单中的电子邮件地址判定为在黑名单中,因为有可能某个好的邮件地址正巧对应个八个都被设置成一的二进制位。好在这种可能性很小。我们把它称为误识概率。在上面的例子中,误识概率在万分之一以下。

布隆过滤器的好处在于快速,省空间。但是有一定的误识别率。常见的补救办法是在建立一个小的白名单,存储那些可能别误判的邮件地址。

以下是Java实现的一个Bloom Filter

import java.util.BitSet;

public class BloomFilter {      
	private int defaultSize = 5000 << 10000;
	private int basic = defaultSize - 1;
	private BitSet bits = new BitSet(defaultSize);

	public BitSet getBits() {
		return bits;
	}

	public void setBits(BitSet bits) {
		this.bits = bits;
	}

	public BloomFilter() {

	}

	private int[] lrandom(String key) {
		int[] randomsum = new int[8];
		int random1 = hashCode(key, 1);
		int random2 = hashCode(key, 2);
		int random3 = hashCode(key, 3);
		int random4 = hashCode(key, 4);
		int random5 = hashCode(key, 5);
		int random6 = hashCode(key, 6);
		int random7 = hashCode(key, 7);
		int random8 = hashCode(key, 8);
		randomsum[0] = random1;
		randomsum[1] = random2;
		randomsum[2] = random3;
		randomsum[3] = random4;
		randomsum[4] = random5;
		randomsum[5] = random6;
		randomsum[6] = random7;
		randomsum[7] = random8;
		return randomsum;
	}

	private void add(String key) {
		if (exist(key)) {
			System.out.println("已经包含(" + key + ")");
			return;
		}
		int keyCode[] = lrandom(key);
		bits.set(keyCode[0]);
		bits.set(keyCode[1]);
		bits.set(keyCode[2]);
		bits.set(keyCode[3]);
		bits.set(keyCode[4]);
		bits.set(keyCode[5]);
		bits.set(keyCode[6]);
		bits.set(keyCode[7]);
	}

	private boolean exist(String key) {
		int keyCode[] = lrandom(key);
		if (bits.get(keyCode[0]) && bits.get(keyCode[1])
				&& bits.get(keyCode[2]) && bits.get(keyCode[3])
				&& bits.get(keyCode[4]) && bits.get(keyCode[5])
				&& bits.get(keyCode[6]) && bits.get(keyCode[7])) {
			return true;
		}
		return false;
	}

	private int hashCode(String key, int Q) {
		int h = 0;
		int off = 0;
		char val[] = key.toCharArray();
		int len = key.length();
		for (int i = 0; i < len; i++) {
			h = (30 + Q) * h + val[off++];
		}
		return changeInteger(h);
	}

	private int changeInteger(int h) {
		return basic & h;
	}

	public static void main(String[] args) {

		String url = "http://www.sina.com/";
		BloomFilter bf = new BloomFilter();
		bf.add(url);
		System.out.println(bf.exist(url));
		System.out.println(bf.exist("www.baidu.com"));

	}
  
}     

ps:如果加大每个信息的指纹数,应该可以降低误识别率。



blog comments powered by Disqus

Published

09 March 2013

Category

algorithm