bloom filter 算法

集合數據結構一般都有這麼一個方法:contains。其作用就是判斷給定的元素是否存在在集合中,這是一個常用的方法。其最簡單的內部實現即遍歷集合內的元素,一個個的判斷是否與給定元素相等。爲了更高效點我們甚至可以採用”更好的(或者相對的)“算法實現。比如如果該集合是已經排序的,那麼我們用二分查找來實現contains肯定更好。但是,如果集合的數據量龐大到一定程度,大部分我們熟知的算法不再有什麼用了。即使可以使用,但是機器內存也不允許。

而bloom filter就是這麼一個空間利用率非常高的算法。我們先來看看這個算法原理:
1、首選我們有一個長度爲n的比特數組,開始的時候將這個比特數組裏所有的元素都 初始化爲o
oooooooooooooooooo
上面的比特數組n爲2o

2、然後選取K個哈希函數,這k個哈希函數產生的結果的值的範圍在0到n-1之間(對於上面的比特數組,即0到19)。對每個要添加進集合的對象進行哈希算法,然後將哈希計算結果作爲數組的索引,將索引位置的比特位設置爲1(不管該比特位原先爲0還是1)。
比如我們選取3個哈希函數,對於對象A哈希值爲0,5,7.那麼比特數組就爲:
1oooo1o1ooooooooooo
對象B的值爲2,8,13,那麼添加B後的比特數組爲:
1o1oo1o11oooo1ooooooo
對象C爲0,4,7(對象C的第一個哈希函數的值與對象A的相同了,沒關係我們還是設置爲1就可以了):
1o1o11o11oooo1oooooooo
現在我們的Bloom Filter裏已經有3個元素了。現在我們要判斷某元素X是否在該集合中。就相當於我們要實現一個contains方法。那麼這個方法如何實現呢?

對元素X採用相同的三個哈希函數哈希,然後以這三個哈希值爲索引去比特數組裏找。如果三個索引位置的比特位都爲1我們就認爲該元素在集合中,否則不是。

我們可以用僞代碼簡單的描述一下這個算法:

public class BloomFilter{
private bit[] bitSet = new bit[N];

public void add(Object element){
  int[] hashValues = getHashValues(element);
  for(int i : hashValues){
     bitSet[i] = 1;
  }
}

public boolean contains(Object element){
    int[] hashValues = getHashValues(element);
    for(int i : hashValues){
       if(bitSet[i] != 1) return false;
    }
    return true;
}

算法還是挺直觀的,對不。想想,一個很大的對象,經過一哈希,然後就變成了Bloom Filter裏面的一個比特,這個空間利用率是多麼高啊。如果哈希函數的實現效率也很高的話那麼不僅空間利用率高,時間複雜度也低啊。這真是一個神奇的算法。

可能你想,以後我就把我們那個啥數組的contains方法替換成bloom filter的實現吧。
不過這個算法有一些問題,

1 如果該元素真的在集合中,那麼Bloom Filter的contains方法肯定會返回true,這就是Bloom Filter不會漏報的特性。

2 如果該元素不在集合中,但Bloom Filter的contains方法有可能返回true。因爲不同的元素經過哈希之後哈希值可能發生碰撞。這是Bloom Filter有可能誤報的特性。但是這個誤報的機率並不高。

根據這兩個特性Bloom Filter在大量數據時還是挺有用的。比如假設我們有一個緩存服務器集羣,集羣裏的不同的服務器承擔的緩存也不盡相同。如果一個用戶請求過來了,我們如何能快速的判斷出用戶請求的這個url在集羣裏哪臺服務器上呢?因爲每臺服務器上緩存的url對應的頁面非常龐大,我們全部弄到內存裏代價也很高。我們就可以在每臺服務器上放一個Bloom Filter,裏面添加的都是本服務器上有緩存的那些url。這樣即使Bloom Filter誤報了,那就是把一個url發到了一個並不持有該url對應的緩存的服務器上,結果就是緩存未命中,緩存服務器只需要將該url打到後端的上游服務器就好了。

根據Bloom Filter的特徵我們可以看到不是所有的場景都可以用的,只有在一些能容許少量的誤報的情況下使用才行。該算法用很低的誤報率卻換來了大量的存儲空間,實在是是一個很巧妙的算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章