題外話:
很久沒寫博客了,因為前一段時間過年在家放假,又因為自己保研了,所以一直比較閑。整個假期,基本都在準備畢業設計的相關內容。我畢業設計的方向是關于搜索引擎的,因此,期間閱讀了大量相關論文。閱讀了很多論文和技術書籍之后,我有幾點感觸。首先,發現國內很多論文或是書籍只是大量引述其他人的研究結果,自己的獨特的見解非常少,一篇文章,70%的內容都是在以介紹為主,感覺發這樣的論文是沒有什么意義的。相反,國外尤其是像MIT,斯坦福,google之類的領域專家發表很多極其優秀的論文,閱讀后給人一種震撼。我之前在網上搜索技術文章或是論文的時候經常繞過英文的,現在發現國外的確有非常多優秀的外文文章,所以無障礙英文閱讀能力是提升自己專業水平的重要因素。 我現在已經逐漸習慣閱讀外文文章和書籍。
期間,我還翻譯了一篇關于網絡信息采集和索引的外文文章,收獲很大。不是因為,我從中獲得了多少知識,而是在翻譯的過程中,我了解了作者精心的設計思路以及嚴謹的語言表達,也鍛煉了我翻譯的能力以及組織語言的能力。
廢話不多說了,開始正題,寫個在爬蟲系統中常用的URL去重經典算法Bloom Filter.
正題:
Bloom Filter概念和原理
引用一篇講述非常好的文章。http://blog.csdn.net/jiaomeng/article/details/1495500,其博客里還有很多關于Bloom filter比較深入的研究,而且博客篇篇都很精彩,非常值得學習。
焦萌 2007年1月27日
Bloom Filter是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,并能判斷一個元素是否屬于這個集合。Bloom Filter的這種高效是有一定代價的:在判斷一個元素是否屬于某個集合時,有可能會把不屬于這個集合的元素誤認為屬于這個集合(false positive)。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter通過極少的錯誤換取了存儲空間的極大節省。
集合表示和元素查詢
下面我們具體來看Bloom Filter是如何用位數組表示集合的。初始狀態時,Bloom Filter是一個包含m位的位數組,每一位都置為0。
為了表達S={x1, x2,…,xn}這樣一個n個元素的集合,Bloom Filter使用k個相互獨立的哈希函數(Hash Function),它們分別將集合中的每個元素映射到{1,…,m}的范圍中。對任意一個元素x,第i個哈希函數映射的位置hi(x)就會被置為1(1≤i≤k)。注意,如果一個位置多次被置為1,那么只有第一次會起作用,后面幾次將沒有任何效果。在下圖中,k=3,且有兩個哈希函數選中同一個位置(從左邊數第五位)。
在判斷y是否屬于這個集合時,我們對y應用k次哈希函數,如果所有hi(y)的位置都是1(1≤i≤k),那么我們就認為y是集合中的元素,否則就認為y不是集合中的元素。下圖中y1就不是集合中的元素。y2或者屬于這個集合,或者剛好是一個false positive。
錯誤率估計
前面我們已經提到了,Bloom Filter在判斷一個元素是否屬于它表示的集合時會有一定的錯誤率(false positive rate),下面我們就來估計錯誤率的大小。在估計之前為了簡化模型,我們假設kn<m且各個哈希函數是完全隨機的。當集合S={x1, x2,…,xn}的所有元素都被k個哈希函數映射到m位的位數組中時,這個位數組中某一位還是0的概率是:
其中1/m表示任意一個哈希函數選中這一位的概率(前提是哈希函數是完全隨機的),(1-1/m)表示哈希一次沒有選中這一位的概率。要把S完全映射到位數組中,需要做kn次哈希。某一位還是0意味著kn次哈希都沒有選中它,因此這個概率就是(1-1/m)的kn次方。令p = e-kn/m是為了簡化運算,這里用到了計算e時常用的近似:
令ρ為位數組中0的比例,則ρ的數學期望E(ρ)= p’。在ρ已知的情況下,要求的錯誤率(false positive rate)為:
(1-ρ)為位數組中1的比例,(1-ρ)k就表示k次哈希都剛好選中1的區域,即false positive rate。上式中第二步近似在前面已經提到了,現在來看第一步近似。p’只是ρ的數學期望,在實際中ρ的值有可能偏離它的數學期望值。M. Mitzenmacher已經證明[2] ,位數組中0的比例非常集中地分布在它的數學期望值的附近。因此,第一步的近似得以成立。分別將p和p’代入上式中,得:
相比p’和f’,使用p和f通常在分析中更為方便。
最優的哈希函數個數
既然Bloom Filter要靠多個哈希函數將集合映射到位數組中,那么應該選擇幾個哈希函數才能使元素查詢時的錯誤率降到最低呢?這里有兩個互斥的理由:如果哈希函數的個數多,那么在對一個不屬于集合的元素進行查詢時得到0的概率就大;但另一方面,如果哈希函數的個數少,那么位數組中的0就多。為了得到最優的哈希函數個數,我們需要根據上一小節中的錯誤率公式進行計算。
先用p和f進行計算。注意到f = exp(k ln(1 − e−kn/m)),我們令g = k ln(1 − e−kn/m),只要讓g取到最小,f自然也取到最小。由于p = e-kn/m,我們可以將g寫成
根據對稱性法則可以很容易看出當p = 1/2,也就是k = ln2· (m/n)時,g取得最小值。在這種情況下,最小錯誤率f等于(1/2)k ≈ (0.6185)m/n。另外,注意到p是位數組中某一位仍是0的概率,所以p = 1/2對應著位數組中0和1各一半。換句話說,要想保持錯誤率低,最好讓位數組有一半還空著。
需要強調的一點是,p = 1/2時錯誤率最小這個結果并不依賴于近似值p和f。同樣對于f’ = exp(k ln(1 − (1 − 1/m)kn)),g’ = k ln(1 − (1 − 1/m)kn),p’ = (1 − 1/m)kn,我們可以將g’寫成
同樣根據對稱性法則可以得到當p’ = 1/2時,g’取得最小值。
位數組的大小
下面我們來看看,在不超過一定錯誤率的情況下,Bloom Filter至少需要多少位才能表示全集中任意n個元素的集合。假設全集中共有u個元素,允許的最大錯誤率為є,下面我們來求位數組的位數m。
假設X為全集中任取n個元素的集合,F(X)是表示X的位數組。那么對于集合X中任意一個元素x,在s = F(X)中查詢x都能得到肯定的結果,即s能夠接受x。顯然,由于Bloom Filter引入了錯誤,s能夠接受的不僅僅是X中的元素,它還能夠є (u - n)個false positive。因此,對于一個確定的位數組來說,它能夠接受總共n + є (u - n)個元素。在n + є (u - n)個元素中,s真正表示的只有其中n個,所以一個確定的位數組可以表示
個集合。m位的位數組共有2m個不同的組合,進而可以推出,m位的位數組可以表示
個集合。全集中n個元素的集合總共有
個,因此要讓m位的位數組能夠表示所有n個元素的集合,必須有
即:
上式中的近似前提是n和єu相比很小,這也是實際情況中常常發生的。根據上式,我們得出結論:在錯誤率不大于є的情況下,m至少要等于n log2(1/є)才能表示任意n個元素的集合。
上一小節中我們曾算出當k = ln2· (m/n)時錯誤率f最小,這時f = (1/2)k = (1/2)mln2 / n。現在令f≤є,可以推出
這個結果比前面我們算得的下界n log2(1/є)大了log2 e ≈ 1.44倍。這說明在哈希函數的個數取到最優時,要讓錯誤率不超過є,m至少需要取到最小值的1.44倍。
總結
在計算機科學中,我們常常會碰到時間換空間或者空間換時間的情況,即為了達到某一個方面的最優而犧牲另一個方面。Bloom Filter在時間空間這兩個因素之外又引入了另一個因素:錯誤率。在使用Bloom Filter判斷一個元素是否屬于某個集合時,會有一定的錯誤率。也就是說,有可能把不屬于這個集合的元素誤認為屬于這個集合(False Positive),但不會把屬于這個集合的元素誤認為不屬于這個集合(False Negative)。在增加了錯誤率這個因素之后,Bloom Filter通過允許少量的錯誤來節省大量的存儲空間。
自從Burton Bloom在70年代提出Bloom Filter之后,Bloom Filter就被廣泛用于拼寫檢查和數據庫系統中。近一二十年,伴隨著網絡的普及和發展,Bloom Filter在網絡領域獲得了新生,各種Bloom Filter變種和新的應用不斷出現。可以預見,隨著網絡應用的不斷深入,新的變種和應用將會繼續出現,Bloom Filter必將獲得更大的發展。
參考資料
[1] A. Broder and M. Mitzenmacher. Network applications of bloom filters: A survey. Internet Mathematics, 1(4):485–509, 2005.
[2] M. Mitzenmacher. Compressed Bloom Filters. IEEE/ACM Transactions on Networking 10:5 (2002), 604—612.
[3] www.cs.jhu.edu/~fabian/courses/CS600.624/slides/bloomslides.pdf
[4] http://166.111.248.20/seminar/2006_11_23/hash_2_yaxuan.ppt
1 /*
2 * bloom.h
3 *
4 * Created on: 2012-2-22
5 * Author: xiaojay
6 */
7
8 #ifndef BLOOM_H_
9 #define BLOOM_H_
10 #include <vector>
11 #include "hashFun.h"
12
13 class Bloom
14 {
15 public :
16 Bloom(int size , std::vector<HashFun*> hashfunclist );
17 ~Bloom();
18 void add(const char * text);
19 bool check(const char * text);
20
21 private :
22 const static int CHARBITSIZE = 8;
23 int size;
24 char * arr;
25 std::vector<HashFun*> hashfunclist;
26 inline void setbit(long pos);
27 inline bool getbit(long pos);
28 };
29
30 #endif /* BLOOM_H_ */
31
32 #include"bloom.h"
33 #include<assert.h>
34
35 Bloom::Bloom(int size , std::vector<HashFun*> hashfunclist)
36 {
37 assert(hashfunclist.size()>0);
38 this->size = size;
39 this->hashfunclist = hashfunclist;
40 this->arr = new char [size];
41 }
42
43 Bloom::~Bloom()
44 {
45 if(this->arr!=NULL)
46 {
47 delete this->arr;
48 }
49 }
50
51 void Bloom::add(const char * text)
52 {
53 int nfunc = hashfunclist.size();
54 long code = 0;
55 for(int i=0;i<nfunc;i++)
56 {
57 code = hashfunclist.at(i)->gethashval(text);
58
59 if(code/CHARBITSIZE>size) return;
60 else
61 {
62 setbit(code);
63 }
64 }
65 }
66
67 bool Bloom::check(const char * text)
68 {
69 int nfunc = hashfunclist.size();
70 long code = 0;
71 for (int i=0;i<nfunc;i++)
72 {
73 code = hashfunclist.at(i)->gethashval(text);
74 if(code/CHARBITSIZE>size) return false;
75 else
76 {
77 if getbit(code) continue;
else return false;
78 }
79 }
return true;
80 }
81
82 inline void Bloom::setbit(long code)
83 {
84 arr[code/CHARBITSIZE] |= (1<<(code%CHARBITSIZE));
85 }
86
87 inline bool Bloom::getbit(long code)
88 {
89 if(!(arr[code/CHARBITSIZE] & (1<<(code%CHARBITSIZE))))
90 {
91 return false;
92 }
93 return true;
94 }
#ifndef HASHFUN_H_
#define HASHFUN_H_
class HashFun
{
public :
virtual long gethashval(const char * key) = 0;
};
#endif /* HASHFUN_H_ */
#include "hashFun.h"
class HashFunA : public HashFun
{
public:
virtual long gethashval(const char * key)
{
unsigned int h=0;
while(*key) h^=(h<<5)+(h>>2)+(unsigned char)*key++;
return h%80000;
}
};
#include"hashFun.h"
class HashFunB : public HashFun
{
public:
virtual long gethashval(const char * key)
{
unsigned int h=0;
while(*key) h=(unsigned char)*key++ + (h<<6) + (h<<16) - h;
return h%80000;
}
};
#include<iostream>
#include<stdio.h>
#include"hashFunA.h"
#include"hashFunB.h"
#include"bloom.h"
#include<vector>
using namespace std;
int main()
{
/*
* Create two hash functions
*/
HashFunA *funa = new HashFunA();
HashFunB * funb = new HashFunB();
vector<HashFun*> hashfunclist;
hashfunclist.push_back(funa);
hashfunclist.push_back(funb);
/*
* Create Bloom object with two parameters :
* size of the store array and list of hash functions
*/
Bloom bloom(10000,hashfunclist);
///Add some words to bloom filter
bloom.add("hello");
bloom.add("world");
bloom.add("ipad");
bloom.add("iphone4");
bloom.add("ipod");
bloom.add("apple");
bloom.add("banana");
bloom.add("hello");
/*
* Test
*/
char word[20];
while(true)
{
cout<<"Please input a word : "<<endl;
cin>>word;
if(bloom.check(word))
{
cout<<"Word :"<<word<<" has been set in bloom filter."<<endl;
}
else
{
cout<<"Word :"<<word<<" not exist !" <<endl;
}
}
return 0;
}
Bloom Filter的基本實現很簡單,但是其原理和思路是非常出色的。這個可以應用到URL去重或是某些緩存系統上。在很多面試題里面,也會出現類似大數據量下去重或是篩選的問題,采用Bloom Filter算法就可以很輕松的解決了。
不含病毒。www.avast.com |
留言列表