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

布隆過濾器是由Burton Bloom與1970年提出來的,因此它的名字就叫作Bloom Filter。它其實是一個很長的二進制向量和一系列的隨機映射函數。php

使用場景

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

算法描述

一個空的布隆過濾器是由m個bits組成的bit array,每個bit位都初始爲0。而且定義有k個不一樣的哈希函數,每一個哈希函數都將元素哈希到bit array的不一樣位置。git

當添加一個元素時,用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)是其中之一。隨着存入元素的數量增長,誤算率隨之增長。ide

誤判機率的證實和計算

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

估算假陽性的機率並不難。假定布隆過濾器有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…

相關文章
相關標籤/搜索