過濾器系列(二)—— Cuckoo filter

這一篇講的是布穀過濾器(cuckoo fliter),這個名字來源於更早發表的布穀散列(cuckoo hash),儘管我也不知道爲何當初要給這種散列表起個鳥名=_=算法

因爲布穀過濾器自己的思想就源自於布穀散列,那麼咱們就從布穀散列開始說它的設計思想。產生布谷散列表的一個重要背景是人們對於球盒問題的分析:給定N個球,隨機的放在N個盒子裏,在裝球最多的盒子裏,球的個數的指望是多少?答案是\(\Theta (logN/loglogN)\),這個問題其實就是散列表裝填因子爲1時的狀況分析。後來有一天,人們發現:每次放球的時候,若是隨機選擇兩個盒子,將球放到當時較空的那個盒子裏,那麼這個指望就變成了\(\Theta (loglogN)\),這個界小於以前的界,這給了布穀散列表做者啓發。app

一個布穀散列表一般有兩張(通常來講)表,分別有一個對應的Hash函數,當有新的項插入的時候,它會計算出這個項在兩張表中對應的兩個位置,這個項必定會被存在這兩個位置之一,而具體是哪個卻不肯定。dom

下圖是一個布穀散列表的初始化示意圖:函數

image

如今咱們假設有一些項要存入散列表,其每一個項都有其對應的兩個位置,先插入第一項A性能

image

因爲插入A的時候其兩個候選位置(0,2)都沒有佔用,因此選擇第一張表或者是第二張表均可以,咱們在這裏默認先選擇第一張表,而後插入第二項B
image測試

咱們看到原來的A的位置被B佔用,而A被「踢」到它的備選位置表二的2號位置上了,這就是當發生位置衝突時,布穀散列表的處理邏輯,後來的數據項將會把以前佔用的項踢到另外一個位置上。咱們接下來插入第三項C優化

image
沒有衝突,順利搞定,接着插入Dthis

image
D成功的把C踢走了,其實看到這裏讀者應該在猜測,會不會有一種狀況,即被踢走的數據的另外一個備選位置也被佔用了,這樣怎麼辦?答案是繼續踢,一個踢一個,直到你們都找到本身合適的歸宿爲止。那麼若是發現出現了循環怎麼辦?答案是GG,這表明布穀散列表走到了極限spa

image
插入E設計

image
這裏就發生了屢次替換的狀況,F代替了E,E代替了A,A代替了B,B找到了空餘的位子

讀者能夠考慮一下,若是這個時候要想插入一個「G」,其備選位置是1,2,這樣的話會出現什麼狀況?

好了,布穀散列表大概介紹完了,如今該布穀過濾器了。布穀過濾器的主要也是經過布穀散列來實現的,其主要變化是:
1.咱們不將原來的數據完整的存進來,做爲過濾器,固然要省點空間,選用的辦法設計一個指紋,將比較大的原數據變成了一個個指紋串,這樣就大大節省了空間。
2.因爲第一點所說,存儲的不是原數據,那麼在替換位置的時候,咱們須要再次計算原來的數據的備選位置,這樣一來布穀散列表的方法就失效了。在這裏,做者設計了一個方法,他將兩個Hash函數變成了一個Hash函數,第一張表的備選位置是Hash(x),第二張表的備選位置是\(Hash(x) \oplus hash(fingerprint(x))\),即第一張表的位置與存儲的指紋的Hash值作異或運算。這樣作的優勢就是不用再另外存儲元素的備選位置,在替換時,能夠直接用異或來計算出其另外一個備選位置。(讀者能夠想一想如何經過表二的位置計算出元素在表一中的位置)
3.插入時,優先選擇空位置,而不是儘量的踢走其餘元素。

插入僞代碼以下:

Algorithm 1: Insert(x)

f = fingerprint(x)
i1 = hash(x)
i2 = i1 xor hash(f)
if bucket[i1] or bucket[i2] has an empty entry then
    //只要有空位就先插入空位裏
    add f to that bucket
    return Done
i  = randomly pick i1 or i2
for n=0;n<MaxNumKicks;++n
    randomly select an entry e from bucket[i];
    swap f and the fingerprint stored in entry e;
    i = i xor hash(f)
    if bucket[i] has an empty entry then
        add f to bucket[i]
        return Done
return Failure // 已經出現循環狀況了

查找僞代碼以下:

Algorithm 2: Lookup(x)
f = fingerprint(x)
i1 = hash(x)
i2 = i1 xor hash(f)
if bucket[i1] or bucket[i2] has f then
    return True
return False

刪除僞代碼以下:

Algorithm 3: Delete(x)

f = fingerprint(x)
i1 = hash(x)
i2 = i1 xor hash(f)
if bucket[i1] or bucket[i2] has f then
    remove a copy of f from this bucket
return False

刪除這部分值得注意,當被刪除元素的另外一個備選位置有其餘元素的指紋的時候,咱們不能肯定到底哪個是要刪除的元素,其實咱們也不去關心究竟是不是要刪除的元素,咱們直接刪除掉其中的一個。這樣一來,咱們其實並無真正的刪除掉元素,爲何這麼說,由於當你刪除掉這個元素的時候咱們再查找這個元素,按照算法來看咱們仍是同樣能檢索出來這個元素在咱們的布穀過濾器裏,這就是假陽率的其中一個來源(還有一個重要來源是指紋構造的重複,即多個元素產生相同指紋)

下面咱們來分析一下布穀過濾器的平均每一個元素佔用的比特數,設每一個桶裏裝\(b\)個指紋,要求錯誤率的上界爲\(\epsilon\)\(f\)爲指紋長度。

錯誤率的上界 = \(1-(1-1/2^f)^{2b} \approx 2b/2^f\)
那麼這個上界要求小於要求的上界,即\(2b/2^f \le \epsilon\),獲得
\(f \ge log_2^{2b/\epsilon} = log_2^{1/\epsilon} + log_2^{2b}\)
則平均每一個元素佔用的比特數爲\(C \le (log_2^{1/\epsilon} + log_2^{2b}) / \alpha\)

在原論文中,做者其實後面還作了一個比較強行的優化,在此不提,後面設計其餘過濾器的做者也沒有把這個優化算數。。。。不過做者提到了在實際測試中,他們發現當b=4的時候是空間性能最好的狀況,因此通常說來,咱們直接把b當作常數4,代入到前面算出來的公式中,\(C \le (log_2^{1/\epsilon} + 3) / \alpha\)

布穀過濾器就說到這,布穀過濾器在錯誤率小於3%的時候空間性能是優於布隆過濾器的,而這個條件在實際應用中經常知足,因此通常來講它的空間性能是要優於布隆過濾器的。並且,布穀過濾器按照普通設計,只有兩個桶,在查找的時候能夠確保兩次訪存就能夠作完,相比於布隆過濾器的K個Hash函數K次訪存,在數據量很大不能所有裝載在內存中的狀況下,多一次訪存那麼時間上就輸了。不過說完優勢,布穀過濾器也有其相應的缺點,當裝填因子較高的時候,容易出現循環的問題,即插入失敗的狀況,到這時就很難辦。另外,它還有跟布隆過濾器共有的一個缺點,就是訪問空間地址不連續,一般能夠認爲是隨機的,這樣嚴重破壞了程序局部性,對於Cache流水線來講很是不利,這爲以後的過濾器設計埋下了一個伏筆。

相關文章
相關標籤/搜索