深夜學(xué)算法之Bloom Filter:概率游戲

1. 前言

Bloom Filter的名字早有耳聞枷畏,但一直沒看實(shí)現(xiàn)原理。今天乘地鐵時(shí)心血來潮看了算法趣兄,頓時(shí)被其簡單與優(yōu)雅震驚木羹。摘錄下wiki上的介紹:

A Bloom filter is a space-efficient probabilistic data structure, conceived by Burton Howard Bloom in 1970, that is used to test whether an element is a member of a set.
Bloom Filter是一種高效利用空間的概率數(shù)據(jù)結(jié)構(gòu),由Burton Howard Bloom于1970年發(fā)明峰锁,用于檢測一個(gè)元素是否屬于一個(gè)集合萎馅。

講個(gè)小插曲,我剛開始以為bloom作「開花虹蒋,繁盛」解糜芳,沒想到是發(fā)明人的名字,真相總是沒有想象的美好呢…

我的實(shí)現(xiàn):https://github.com/liquidconv/DSAF

2. 學(xué)習(xí)Bloom Filter

2.1 爬蟲與集合操作

Bloom Filter通常和爬蟲聯(lián)系在一起魄衅,所以用這個(gè)例子解釋其特性再好不過峭竣。爬蟲最常用的抓取方法是廣度優(yōu)先搜索BFS,概括地說就是:

  1. 維護(hù)一個(gè)待訪問鏈接的隊(duì)列
  2. 每次將隊(duì)首元素晃虫,即里第一個(gè)鏈接出隊(duì)
  3. 將該鏈接對(duì)應(yīng)的頁面上所有鏈接入隊(duì)
  4. 重復(fù)1~3至隊(duì)列空為止

上面的描述里存在一個(gè)很大的問題——第三步里要加入隊(duì)列的鏈接可能沒有訪問過皆撩,可能已經(jīng)訪問過,如果不做判斷一律加入的話哲银,一個(gè)相同的頁面就可能被訪問幾千幾萬次扛吞,造成資源浪費(fèi)。

要解決也很簡單盘榨,只要維護(hù)一個(gè)訪問過鏈接的集合就可以了喻粹。鏈接入隊(duì)之前,先判斷是否屬于該集合草巡,即是否已經(jīng)訪問過了守呜,屬于就不入隊(duì),不屬于才入隊(duì)山憨。

2.2 思考

我們需要一個(gè)「集合」數(shù)據(jù)結(jié)構(gòu)查乒,至少要能夠支持插入元素到集合和判斷元素是否屬于集合——也就是插入和屬于這兩種操作,要怎么實(shí)現(xiàn)呢郁竟?

可行思路:數(shù)組/鏈表/樹/哈希表

暫時(shí)不考慮插入和查找操作的時(shí)間復(fù)雜度玛迄,上面每種做法里數(shù)據(jù)單元(數(shù)組的元素,鏈表和樹的節(jié)點(diǎn)棚亩,哈希表的表項(xiàng))定義形式基本都是:

typedef struct data_item {
    key k;     // 原始數(shù)據(jù)蓖议,或者原始數(shù)據(jù)的hash值等
    extra e ;  // 附加信息,指向其它節(jié)點(diǎn)的指針等
};

每次向集合插入元素都要生成新的data_item讥蟆,空間復(fù)雜度是O(N)勒虾,大數(shù)據(jù)情況下hold不住。

算法里經(jīng)常有時(shí)空權(quán)衡的問題瘸彤,可以用空間換時(shí)間保存子問題結(jié)果修然,比如動(dòng)態(tài)規(guī)劃;可以用時(shí)間換空間,比如用泰勒展開計(jì)算三角函數(shù)愕宋。當(dāng)時(shí)間和空間都不能犧牲的時(shí)候玻靡,就只能犧牲正確率了。Bloom Filter之所以稱為概率數(shù)據(jù)結(jié)構(gòu)中贝,就是因?yàn)樗牟僮鹘Y(jié)果有一定概率是錯(cuò)誤的囤捻。

2.3 圖解Bloom Filter

Bloom Filter的核心是一個(gè)m位的bitset和k個(gè)hash函數(shù)。

初始時(shí)bitset中所有位的值都設(shè)置為0雄妥,假設(shè)取m = 10最蕾,k = 3依溯,用藍(lán)色表示某位為0老厌,紅色表示為1:

初始化時(shí)的bitset

插入元素的過程是三步走:

  1. 計(jì)算k個(gè)hash值
  2. 將k個(gè)hash值對(duì)m取模得到k個(gè)下標(biāo)
  3. 將bitset中k個(gè)下標(biāo)對(duì)應(yīng)的位設(shè)置為1

比如向剛才的Bloom Filter插入元素“Alice”。分別用3個(gè)hash函數(shù)計(jì)算“Alice”的hash值黎炉,將hash值對(duì)10取模枝秤,得到在[0, 10)范圍內(nèi)的r1、r2慷嗜、r3淀弹,假設(shè)計(jì)算結(jié)果為:

r1 = h1("Alice") % m = 1
r2 = h2("Alice") % m = 3
r3 = h3("Alice") % m = 5

于是將bitset中第1位、第3位和第5位的值置為1:

第一次插入后的bitset

再插入元素“Bob”的過程還是一樣的庆械,假設(shè):

r1 = h1("Bob") % m = 8
r2 = h2("Bob") % m = 2
r3 = h3("Bob") % m = 3

那就將bitset中第2位薇溃、第3位、第8位的值置為1(值已經(jīng)為1的第3位不動(dòng)):

第二次插入后的bitset

圖形化思考的話就是缭乘,Bloom Filter運(yùn)行過程中不斷插入新元素沐序,bitset里的0逐漸被翻轉(zhuǎn)成1。

怎么判斷元素“Alice”是否在集合里呢堕绩?同樣還是三步走:

  1. 計(jì)算k個(gè)hash值
  2. 將k個(gè)hash值對(duì)m取模得到k個(gè)下標(biāo)
  3. 檢查bitset中k個(gè)下標(biāo)對(duì)應(yīng)的位是否都為1

如果Bloom Filter里有“Alice”策幼,那bitset中相應(yīng)的k位值顯然都為1。問題是即使Bloom Filter里沒有“Alice”奴紧,還是可能由于之前插入的元素而導(dǎo)致“Alice”對(duì)應(yīng)的k位值都為1特姐,因此會(huì)錯(cuò)誤地認(rèn)為集合里已經(jīng)有“Alice”了,這就是Bloom Filter會(huì)出錯(cuò)的地方黍氮。

由于bitset里每位都和多個(gè)元素有關(guān)唐含,將某個(gè)為1的位置為0,涉及到這位的元素都會(huì)被認(rèn)為不屬于集合沫浆,所以Bloom Filter不支持刪除操作捷枯。

2.4 復(fù)雜度分析

空間復(fù)雜度方面,Bloom Filter不會(huì)動(dòng)態(tài)增長件缸,運(yùn)行過程中維護(hù)的始終只是m位的bitset铜靶,所以空間復(fù)雜度只有O(m)。

時(shí)間復(fù)雜度方面,Bloom Filter的插入與屬于操作主要都是在計(jì)算k個(gè)hash争剿,所以都是O(k)已艰。

3. 實(shí)現(xiàn)Bloom Filter

3.1 定義Bloom Filter類

STL里的bitset定義時(shí)就要給定長度,而且之后不能改變蚕苇,所以我用了vector<bool>哩掺。

#ifndef __BLOOM_FILTER_H__
#define __BLOOM_FILTER_H__

#include <vector>
#include <string>

class BloomFilter {
    public:
        BloomFilter(unsigned int m, unsigned int k);
        void insertElement(std::string s);
        bool existsElement(std::string s);
    private:
        unsigned int BKDR_Hash(std::string s, unsigned int i);
        unsigned int getSeed(unsigned int hash_index);
        std::vector<bool> table;
        unsigned int _m;
        unsigned int _k;
};

#endif // __BLOOM_FILTER_H__

定義非常簡單:

  • _m是bitset的大小,table是bitset
  • _k是hash函數(shù)的數(shù)量
  • BKDR_Hash是使用了參數(shù)seed的hash函數(shù)
  • 用getSeed生成_k種seed涩笤,相當(dāng)于有了_k種hash函數(shù)
  • insertElement對(duì)于插入操作
  • existsElement對(duì)應(yīng)查找操作

構(gòu)造函數(shù)

BloomFilter::BloomFilter(unsigned int m, unsigned int k):
    _m(m), _k(k) {
        table.resize(_m);
        for(int i = 0; i < _m; ++i)
            table[i] = false;
}

3.2 插入與屬于

插入操作 = 計(jì)算_k個(gè)hash值嚼吞,將table中對(duì)應(yīng)位置改為true:

void BloomFilter::insertElement(string s) {
    for(int i = 0; i < _k; ++i) {
        unsigned int index = BKDR_Hash(s, i);
        table[index] = true;
    }
}

屬于操作 = 計(jì)算_k個(gè)hash值,檢查table中對(duì)應(yīng)位置是否都為true:

bool BloomFilter::existsElement(string s) {
    for(int i = 0; i < _k; ++i) {
        unsigned int index = BKDR_Hash(s, i);
        if(!table[index])
            return false;
    }
    return true;
}

3.3 BKDR Hash

選擇BKDR Hash最主要的原因就是有參數(shù)seed蹬碧,很容易就能構(gòu)造一族Hash函數(shù)舱禽。seed是個(gè)「魔數(shù)」(magic number),所以不用多糾結(jié)恩沽,推薦取的值為:
131,1313,13131,...

生成第hash_index個(gè)Hash函數(shù)的seed:

unsigned int BloomFilter::getSeed(unsigned int hash_index) {
    string seed = "13";
    for(int i = 0; i < hash_index; ++i)
        seed += (i % 2 == 0)? "1" : "3";

    return atoi(seed.c_str());
}

用第hash_index個(gè)Hash函數(shù)計(jì)算字符串s的hash值:

unsigned int BloomFilter::BKDR_Hash(string s, unsigned int hash_index) {
    const char *ps = s.c_str();
    unsigned int seed = getSeed(hash_index);
    unsigned int hash = 0;

    for(int i = 0; i < s.size(); ++i)
        hash += hash * seed + ps[i];

    return hash % _m;
}

4. 數(shù)學(xué)部分

Bloom Filter的原理已經(jīng)講完誊稚,但還是有必要提一下錯(cuò)誤率的問題。錯(cuò)誤率有兩種:

  • FP = false positive
  • FN = false negative

對(duì)應(yīng)Bloom Filter的情況下罗心,F(xiàn)P就是「集合里沒有某元素里伯,查找結(jié)果是有該元素」,F(xiàn)N就是「集合里有某元素渤闷,查找結(jié)果是沒有該元素」疾瓮。FN顯然總是0,F(xiàn)P會(huì)隨著Bloom Filter中插入元素的數(shù)量而增加——極限情況就是所有bit都為1飒箭,這時(shí)任何元素都會(huì)被認(rèn)為在集合里狼电。

FP的推導(dǎo)并不復(fù)雜,wiki上有非常詳細(xì)的過程补憾,這里就簡單地抄個(gè)結(jié)果漫萄,其中n是當(dāng)前集合里元素的數(shù)量:

FP表達(dá)式,截自wiki

從這個(gè)公式里可以讀出一些直觀的信息:

  • n = 0時(shí)盈匾,F(xiàn)P = 0腾务;n趨于無窮大時(shí),F(xiàn)P趨于1
  • k/m和n保持不變時(shí)削饵,k越大岩瘦,F(xiàn)P越小

m和k決定了Bloom Filter的「容量」,當(dāng)然hash函數(shù)的選擇也很重要窿撬。

5. 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末启昧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子劈伴,更是在濱河造成了極大的恐慌密末,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異严里,居然都是意外死亡新啼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門刹碾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來燥撞,“玉大人,你說我怎么就攤上這事迷帜∥锸妫” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵戏锹,是天一觀的道長冠胯。 經(jīng)常有香客問我,道長景用,這世上最難降的妖魔是什么涵叮? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮伞插,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘盾碗。我一直安慰自己媚污,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布廷雅。 她就那樣靜靜地躺著耗美,像睡著了一般航缀。 火紅的嫁衣襯著肌膚如雪芥玉。 梳的紋絲不亂的頭發(fā)上赶袄,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音敬辣,去河邊找鬼溉跃。 笑死喊积,一個(gè)胖子當(dāng)著我的面吹牛乾吻,可吹牛的內(nèi)容都是我干的枯饿。 我是一名探鬼主播奢方,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了忠聚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎欢瞪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骑祟,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怯晕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年堵第,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了踏志。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片针余。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖摸柄,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤酪术,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站庐舟,受9級(jí)特大地震影響历帚,放射性物質(zhì)發(fā)生泄漏挽牢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一太惠、第九天 我趴在偏房一處隱蔽的房頂上張望凿渊。 院中可真熱鬧搪锣,春花似錦构舟、人聲如沸狗超。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至殴胧,卻和暖如春渗稍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背团滥。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國打工竿屹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灸姊。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓拱燃,卻偏偏與公主長得像,于是被迫代替她去往敵國和親厨钻。 傳聞我的和親對(duì)象是個(gè)殘疾皇子扼雏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 布隆過濾器 Bloom Filter 布隆過濾器坚嗜,用來判斷一個(gè)元素是否在集合中。它的特點(diǎn)是節(jié)省空間诗充,但是有誤判苍蔬。有...
    周肅閱讀 4,543評(píng)論 0 5
  • 布隆過濾器 (Bloom Filter) 詳解 原文鏈接:http://www.cnblogs.com/allen...
    JackChen1024閱讀 11,838評(píng)論 0 3
  • 教你如何迅速秒殺掉:99%的海量數(shù)據(jù)處理面試題 本文經(jīng)過大量細(xì)致的優(yōu)化后,收錄于我的新書《編程之法》第六章中蝴蜓,新書...
    Helen_Cat閱讀 7,407評(píng)論 1 39
  • (一)——開篇 大數(shù)據(jù)量的問題是很多面試筆試中經(jīng)常出現(xiàn)的問題碟绑,比如baidu google 騰訊 這樣的一些涉及到...
    零一間閱讀 724評(píng)論 0 5
  • Bloom Filter 算法 初始狀態(tài)下,Bloom Filter是一個(gè)m位的位數(shù)組茎匠,且數(shù)組被0所填充格仲。同時(shí),我...
    Q南南南Q閱讀 994評(píng)論 0 1