如何在億級數據中判斷一個元素是否存在?

前言

在平常工做中,常常要判斷一個元素是否在一個集合中。假設你要向瀏覽器添加一項功能,該功能能夠通知用戶輸入的網址是不是惡意網址,此時你手上有大約 1000 萬個惡意 URL 的數據集,你該如何實現該功能。按我以前的思惟,要判斷一個元素在不在當前的數據集中,首先想到的就是使用 hash table,經過哈希函數運行全部的惡意網址以獲取其哈希值,而後建立出一個哈希表(數組)。這個方案有個明顯的缺點,就是須要存儲原始元素自己,內存佔用大,而咱們其實主要是關注 當前輸入的網址在不在咱們的惡意 URL 數據集中,也就是以前的惡意 URL 數據集的具體值是什麼並不重要,經過吳軍老師的《數學之美》瞭解到,對於這種場景大數據領域有個用於在海量數據狀況下判斷某個元素是否已經存在的算法很適合,關鍵的一點是該算法並不存儲元素自己,這個算法就是 — 布隆過濾器(Bloom filter)。算法

原理

布隆過濾器是由巴頓.布隆於一九七零年提出的,在 維基百科 中的描述以下:數組

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.瀏覽器

布隆過濾器是一個數據結構,它能夠用來判斷某個元素是否在集合內,具備運行快速,內存佔用小的特色,它由一個很長的二進制向量和一系列隨機映射函數組成。而高效插入和查詢的代價就是,它是一個基於機率的數據結構,只能告訴咱們一個元素絕對不在集合內,布隆過濾器的好處在於快速,省空間,可是有必定的誤判率。布隆過濾器的基礎數據結構是一個比特向量,假設有一個長度爲 16 的比特向量,下面咱們經過一個簡單的示例來看看其工做原理,:數據結構

bloom-filter-bit-array.png

上圖比特向量中的每個空格表示一個比特, 空格下面的數字表示當前位置的索引。只須要簡單的對輸入進行屢次哈希操做,並把對應於其結果的比特置爲 1,就完成了向 Bloom filter 添加一個元素的操做。下圖表示向布隆過濾器中添加元素 https://www.mghio.cnhttps://www.abc.com 的過程,它使用了 func1func2 兩個簡單的哈希函數。函數

bloom-filter-add-item.png

當咱們往集合裏添加一個元素的時候, 能夠檢查該元素在應用對應哈希函數後的哈希值對比特向量的長度取餘後的位置是否爲 1,圖中用 1 表示最新添加的元素對應位置。而後當咱們要判斷添加元素是否存在集合中的話,只須要簡單的經過對該元素應用一樣的哈希函數,而後看比特向量裏對應的位置是否爲 1 的方式來判斷一個元素是否在集合裏。若是不是,則該元素必定再也不集合中,可是須要注意的是,若是是,你只知道元素可能在裏面, 由於這些對應位置有可能恰巧是由其它元素或者其它元素的組合所引發的。以上就是布隆過濾器的實現原理。大數據

如何本身實現

布隆過濾器的思想比較簡單,首先在構造方法中初始化了一個指定長度的 int 數組,在添加元素的時候經過哈希函數 func1func2 計算出對應的哈希值,對數組長度取餘後將對應位置置爲 1,判斷元素是否存在於集合中時,一樣也是對元素用一樣的哈希函數進行兩次計算,取到對應位置的哈希值,只要存在位置的值爲 0,則認爲元素不存在。下面使用 Java 語言實現了上面示例中簡單版的布隆過濾器:優化

public class BloomFilter {

  /** * 數組長度 */
  private int size;

  /** * 數組 */
  private int[] array;

  public BloomFilter(int size) {
    this.size = size;
    this.array = new int[size];
  }

  /** * 添加數據 */
  public void add(String item) {
    int firstIndex = func1(item);
    int secondIndex = func2(item);
    array[firstIndex % size] = 1;
    array[secondIndex % size] = 1;
  }

  /** * 判斷數據 item 是否存在集合中 */
  public boolean contains(String item) {
    int firstIndex = func1(item);
    int secondIndex = func2(item);
    int firstValue = array[firstIndex % size];
    int secondValue = array[secondIndex % size];
    return firstValue != 0 && secondValue != 0;
  }

  /** * hash 算法 func1 */
  private int func1(String key) {
    int hash = 7;
    hash += 61 * hash + key.hashCode();
    hash ^= hash >> 15;
    hash += hash << 3;
    hash ^= hash >> 7;
    hash += hash << 11;
    return Math.abs(hash);
  }

  /** * hash 算法 func2 */
  private int func2(String key) {
    int hash = 7;
    for (int i = 0, len = key.length(); i < len; i++) {
      hash += key.charAt(i);
      hash += (hash << 7);
      hash ^= (hash >> 17);
      hash += (hash << 5);
      hash ^= (hash >> 13);
    }
    return Math.abs(hash);
  }
} 
複製代碼

本身實現雖然簡單可是有一個問題就是檢測的誤判率比較高,經過其原理能夠知道,可咱們能夠提升數組長度以及 hash 計算次數來下降誤報率,可是相應的 CPU、內存的消耗也會相應的提升;這須要咱們根據本身的業務須要去權衡選擇。this

扎心一問

哈希函數該如何設計?

布隆過濾器裏的哈希函數最理想的狀況就是須要儘可能的彼此獨立且均勻分佈,同時,它們也須要儘量的快 (雖然 sha1 之類的加密哈希算法被普遍應用,可是在這一點上考慮並非一個很好的選擇)。加密

布隆過濾器應該設計爲多大?

我的認爲布隆過濾器的一個比較好特性就是咱們能夠修改過濾器的錯誤率。一個大的過濾器會擁有比一個小的過濾器更低的錯誤率。假設在布隆過濾器裏面有 k 個哈希函數,m 個比特位(也就是位數組長度),以及 n 個已插入元素,錯誤率會近似於 (1-ekn/m)k,因此你只須要先肯定可能插入的數據集的容量大小 n,而後再調整 k 和 m 來爲你的應用配置過濾器。spa

應該使用多少個哈希函數?

顯然,布隆過濾器使用的哈希函數越多其運行速度就會越慢,可是若是哈希函數過少,又會遇到誤判率高的問題。因此這個問題上須要認真考慮,在建立一個布隆過濾器的時候須要肯定哈希函數的個數,也就是說你須要提早預估集合中元素的變更範圍。然而你這樣作了以後,你依然須要肯定比特位個數和哈希函數的個數的值。看起來這彷佛這是一個十分困難的優化問題,但幸運的是,對於給定的 m(比特位個數)和 n(集合元素個數),最優的 k(哈希函數個數)值爲: (m/n)ln(2)(PS:須要瞭解具體的推導過程的朋友能夠參考維基百科)。也就是咱們能夠經過如下步驟來肯定布隆過濾器的哈希函數個數:

  1. 肯定 n(集合元素個數)的變更範圍。
  2. 選定 m(比特位個數)的值。
  3. 計算 k(哈希函數個數)的最優值

對於給定的 n、m 和 k 計算錯誤率,若是這個錯誤率不能接受的話,能夠繼續回到第二步。

布隆過濾器的時間複雜度和空間複雜度?

對於一個 m(比特位個數)和 k(哈希函數個數)值肯定的布隆過濾器,添加和判斷操做的時間複雜度都是 O(k),這意味着每次你想要插入一個元素或者查詢一個元素是否在集合中,只須要使用 k 個哈希函數對該元素求值,而後將對應的比特位標記或者檢查對應的比特位便可。

總結

布隆過濾器的實際應用很普遍,特別是那些要在大量數據中判斷一個元素是否存在的場景。能夠看到,布隆過濾器的算法原理比較簡單,但要實際作一個生產級別的布隆過濾器仍是很複雜的,谷歌的開源庫 GuavaBloomFilter 提供了 Java 版的實現,用法很簡單。最後留給你們一個問題:布隆過濾器支持元素刪除嗎?

相關文章
相關標籤/搜索