那些有趣的演算法之布隆過濾器

sizeofio發表於2019-02-26

布隆過濾器是由Burton Bloom與1970年提出來的,所以它的名字就叫做Bloom Filter。它實際上是一個很長的二進位制向量和一系列的隨機對映函式。

使用場景

  1. 有的黑客為了讓服務當機,他們會構建大量不存在於快取中的key向伺服器發起請求,在資料量足夠大的情況下,頻繁的資料庫查詢可能導致DB掛掉。布隆過濾器很好的解決了快取擊穿的問題。
  2. 反垃圾郵件,從數十億個垃圾郵件列表中判斷某個郵箱是否是垃圾郵箱。
  3. 網頁爬蟲對URL去重,防治爬取相同的URL地址
  4. ...

演算法描述

一個空的布隆過濾器是由m個bits組成的bit array,每一個bit位都初始為0。並且定義有k個不同的雜湊函式,每個雜湊函式都將元素雜湊到bit array的不同位置。

當新增一個元素時,用k個雜湊函式分別將它hash得到k個bit位,然後將這些bit位置位1。

查詢一個函式時,同樣用k個雜湊函式將它hash,再判斷k個bit位上是否都為1,如果其中某一位為0,則該元素不存在於布隆過濾器中。

常規的布隆過濾器不允許執行刪除元素操作,因為那樣會把k個bits位置位0,而其中某一位可能和其他元素想對應。因此刪除操作會引入false negative,如果需要刪除操作可以使用Counting Bloom Filter

enter image description here

當k很大時,設計k個獨立的雜湊函式是不現實的。對於一個輸出範圍很大的雜湊函式(MD5產生的128 bits),如果不同bits的相關性很小,則可以把此輸出分割位k份。或者將k個不同的初始值結合元素,feed給一個雜湊函式從而產生k個不同的值。

舉例說明

就以垃圾郵件過濾為例,假定我們有一億個垃圾郵件地址,每個郵件用8個hash函式來生成8個資訊指紋,因為在保證誤判率低且k和m選取合適時,空間利用率為50%。所以我們的m(布隆過濾器的槽數)為

enter image description here
,也就是16億個二進位制位。我們先將所有二進位制位全部清零。對於每個郵件地址X,我們用8個不同的hash函式進行hash,再將這8個資訊指紋對映到1-16億中的8個自然數g1,g2,...g8。現在將這8個位置的二進位制值全部置為1。對一億個郵件地址都進行這樣的處理後,我們的布隆過濾器也就建成功了。

enter image description here

當我們要判斷一個郵件地址是否在布隆過濾器中時,需要使用相同的8個hash函式來將8個資訊指紋對應到布隆過濾器的8個二進位制位上。如果8個二進位制位的值只要有一個或更多為0,那麼它一定不存在於布隆中。如果8個值全都為1,那麼它可能存在於布隆中,這是因為誤識別導致的。

優勢

相對於其它的資料結構,布隆過濾器在空間和時間方面都有巨大的優勢。布隆過濾器儲存空間和插入/查詢時間都是常數。另外,hash函式相互之間沒有關係,方便由硬體並行實現。布隆過濾器不需要儲存資料本身,在某些對保密要求非常嚴格的場合由優勢。

缺點

布隆過濾器的缺點和其優點一樣明顯。誤算率(False Positive)是其中之一。隨著存入元素的數量增加,誤算率隨之增加。

誤判概率的證明和計算

在上面的案例中,我們說到過關於布隆的誤算率的問題,這在檢驗上被稱為假陽性

估算假陽性的概率並不難。假定布隆過濾器有m位元,裡面有n個元素,每個元素對應k個資訊指紋的雜湊函式,當然這裡m位元里有些是0有些是1。我們先來看看某個位元為0的概率。當我們在插入一個元素時,它的第一個雜湊函式會把過濾器中的某個位元置為1,因此,任何一個位元被置為1的概率是1/m,它依然為0的概率則為1-1/m。對於過濾器中的某個特定位置,如果這個元素k個雜湊函式都沒有把它設定為1,其概率是(1-1/m)^k。如果過濾器插入第二個元素,某個特定位置依然沒有被設定為1,其概率為(1-1/m)^2k。如果插入了n個元素,還是沒有把某個位置設定為1,其概率為(1-1/m)^kn。反過來,一個位元在插入了n個元素後,被置為1的概率為1-(1-1/m)^kn

現在假定這n個元素都放到了過濾器中,新來一個不在集合中的元素,由於它的資訊指紋的雜湊函式都是隨機的,因此,它的第一個雜湊函式正好命中某個值為1的位元的概率就是上述概率。一個不在集合中的元素被誤識別為在集合中,所需要的雜湊函式對應位元的值均為1,其概率為:

enter image description here

化簡後為:

enter image description here

如果n比較大,可以近似為:

enter image description here

PHP實現

class BloomFilterHash
{
    /**
     * 由Justin Sobel 編寫的按位雜湊函式.
     *
     * @param string $string
     * @param null $len
     * @return int
     */
    public function JSHash($string, $len = null)
    {
        $hash = 1315423911;
        $len || $len = strlen($string);
        for ($i = 0; $i < $len; $i ++) {
            $hash ^= (($hash << 5) + ord($string[$i]) + ($hash >> 2));
        }

        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 該雜湊演算法基於AT&T貝爾實驗室的Peter J. Weinberger的工作。
     * Aho Sethi和Ulman編寫的“編譯器(原理,技術和工具)”一書建議使用採用此特定演算法中的雜湊方法的雜湊函式。
     *
     * @param string $string
     * @param null $len
     * @return int
     */
    public function PJWHash($string, $len = null)
    {
        $bitsInUnsignedInt = 4 * 8;
        $threeQuarters = ($bitsInUnsignedInt * 3) / 4;
        $oneEighth = $bitsInUnsignedInt / 8;
        $highBits = 0xFFFFFFFF << (int) ($bitsInUnsignedInt - $oneEighth);
        $hash = 0;
        $test = 0;
        $len || $len = strlen($string);
        for ($i = 0; $i < $len; $i ++) {
            $hash = ($hash << (int) ($oneEighth)) + ord($string[$i]);
        }
        $test = $hash & $highBits;
        if ($test != 0) {
            $hash = (($hash ^ ($test >> (int)($threeQuarters))) & (~$highBits));
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 類似PJW Hash功能,但是針對32位處理器做了調整。它是基於unix系統上的widely使用雜湊函式。
     *
     * @param string $string
     * @param null $len
     * @return int
     */
    public function ELEHash($string, $len = null)
    {
        $hash = 0;
        $len || $len = strlen($string);
        for ($i = 0; $i < $len; $i++) {
            $hash = ($hash << 4) + ord($string[$i]);
            $x = $hash & 0xF0000000;
            if ($x != 0) {
                $hash ^= ($x >> 24);
            }
            $hash &= ~$x;
        }

        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 這個雜湊函式來自Brian Kernighan和Dennis Ritchie的書“The C Programming Language”。
     * 它是一個簡單的雜湊函式,使用一組奇怪的可能種子,它們都構成了31 .... 31 ... 31等模式,它似乎與DJB雜湊函式非常相似。
     */
    public function BKDRHash($string, $len = null)
    {
        $seed = 131;  # 31 131 1313 13131 131313 etc..
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash * $seed) + ord($string[$i]));
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 這是在開源SDBM專案中使用的首選演算法。
     * 雜湊函式似乎對許多不同的資料集具有良好的總體分佈。它似乎適用於資料集中元素的MSB存在高差異的情況。
     */
    public function SDBMHash($string, $len = null)
    {
        $hash = 0;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (ord($string[$i]) + ($hash << 6) + ($hash << 16) - $hash);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 由Daniel J. Bernstein教授製作的演算法,首先在usenet新聞組comp.lang.c上向世界展示。
     * 它是有史以來發布的最有效的雜湊函式之一。
     */
    public function DJBHash($string, $len = null)
    {
        $hash = 5381;
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) (($hash << 5) + $hash) + ord($string[$i]);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * Donald E. Knuth在“計算機程式設計藝術第3卷”中提出的演算法,主題是排序和搜尋第6.4章。
     */
    public function DEKHash($string, $len = null)
    {
        $len || $len = strlen($string);
        $hash = $len;
        for ($i=0; $i<$len; $i++) {
            $hash = (($hash << 5) ^ ($hash >> 27)) ^ ord($string[$i]);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }

    /**
     * 參考 http://www.isthe.com/chongo/tech/comp/fnv/
     */
    public function FNVHash($string, $len = null)
    {
        $prime = 16777619; //32位的prime 2^24 + 2^8 + 0x93 = 16777619
        $hash = 2166136261; //32位的offset
        $len || $len = strlen($string);
        for ($i=0; $i<$len; $i++) {
            $hash = (int) ($hash * $prime) % 0xFFFFFFFF;
            $hash ^= ord($string[$i]);
        }
        return ($hash % 0xFFFFFFFF) & 0xFFFFFFFF;
    }
}
複製程式碼
abstract class BloomFilterRedis
{
    /**
     * 需要使用一個方法來定義bucket名字.
     */
    protected $bucket;

    protected $hashFunction;

    public function __construct()
    {
        if (!$this->bucket || !$this->hashFunction) {
            throw new Exception("需要定義bucket和hashFunction");
        }

        $this->Hash = new BloomFilterHash;
        $this->Redis = new \Redis();   // 假設已經連線好了
        $this->Redis->connect('127.0.0.1');
    }

    /**
     * @param $string
     * @return array
     */
    public function add($string)
    {
        $pipe = $this->Redis->multi();
        foreach ($this->hashFunction as $function) {
            $hash = $this->Hash->$function($string);
            $pipe->setBit($this->bucket, $hash, 1);
        }
        return $pipe->exec();
    }

    /**
     * 查詢是否存在,不存在的一定不存在,存在的可能存在誤判.
     *
     * @param $string
     * @return bool
     */
    public function exists($string)
    {
        $pipe = $this->Redis->multi();
        $len = strlen($string);
        foreach ($this->hashFunction as $function) {
            $hash = $this->Hash->$function($string, $len);
            $pipe = $pipe->getBit($this->bucket, $hash);
        }
        $res = $pipe->exec();
        foreach ($res as $bit) {
            if ($bit == 0) {
                return false;
            }
        }

        return true;
    }
}
複製程式碼
class FilteRepeatedComments extends BloomFilterRedis
{
    protected $bucket = 'rptc';

    protected $hashFunction = array('BKDRHash', 'SDBMHash', 'JSHash');
}
複製程式碼

小結

原始碼地址:gitee.com/pchangl/Blo…

相關文章