布隆過濾器(Bloom Filters)的原理及代碼實現(Python + Java)

本文介紹了布隆過濾器的概念及變體,這種描述很是適合代碼模擬實現。重點在於標準布隆過濾器和計算布隆過濾器,其餘的大都在此基礎上優化。文末附上了標準布隆過濾器和計算布隆過濾器的代碼實現(Java版和Python版)java

本文內容皆來自 《Foundations of Computers Systems Research》一書,本身翻譯的,轉載請註明出處,不許確的部分請告知,歡迎討論。算法

 

 

  • 布隆過濾器是什麼?

    布隆過濾器是一個高效的數據結構,用於集合成員查詢,具備很是低的空間複雜度。
     
 
  • 標準布隆過濾器(Standard Bloom Filters,SBF)

    基本狀況數組

    布隆過濾器是一個含有 m 個元素的位數組(元素爲0或1),在剛開始的時候,它的每一位都被設爲0。同時還有 k 個獨立的哈希函數 h1, h2,..., hk 。須要將集合中的元素加入到布隆過濾器中,而後就能夠支持查詢了。說明以下:

    1. 計算h1(x), h2(x),...,hk(x),其計算結果對應數組的位置,並將其所有置1。一個位置能夠被屢次置1,但只有一次有效。
    2. 當查詢某個元素是否在集合中時,計算這 k 個哈希函數,只有當其計算結果所有爲1時,咱們就認爲該元素在集合內,不然認爲不在。
    3. 布隆過濾器存在假陽性的可能,即當全部哈希值都爲1時,該元素也可能不在集合內,但該算法認爲在裏面。
    4. 假陽性出現的機率被哈希函數的數量、位數組大小、以及集合元素等因素決定。

     

    假陽性率評估
     
    爲了評估假陽性率,須要基於一個假設:哈希函數都是完美隨機的。約定幾個變量:
     
    1. k 哈希函數的數量
    2. n 集合 S 中元素的數量
    3. m 位數組的大小
    4. p 位數組中某一位爲0的機率
    5. f 假陽性的機率

     

    最後得出:

                                    

     最佳的哈希函數數量緩存


    根據數學推理得(過程就算了):當 p = 1/2, k = ln2 * (m/n)時,f 最小爲(1/2)^k

    能夠看出,當位數組中有一半零一半一時,結果最好。
    事實上,m 是 n 的倍數,並且 k 常取最接近但小於理論值的整數值。

    服務器

    部分佈隆過濾器(partial bloom filters)
     
     
  • 計算布隆過濾器(Counting Bloom Filters,CBF)

    標準的布隆過濾器有一個致命的缺點:不支持刪除元素。CBF協議解決的這個問題。
    1. 將標準布隆過濾器中的位數組變成整數數組,便可以用多位表示。
    2. 標準布隆過濾器每一個位置能夠被屢次置1,但只有一次有效,這樣,某一個位置被多個元素哈希映射,當要刪除其中一個元素時,該元素哈希映射的位置都應該變爲零,那麼就會破壞其餘元素的映射,會出現假陰性。
    3. 因爲計算布隆過濾器的數組能夠表示更大的整數,那麼當某個位置被映射到時,該位置的計數值就自增1,而當某個元素被刪除時,就將其映射位置的計數值減1。這樣就解決了SBF的問題。
    4. CBF一樣存在問題,由於當計數值自增時可能會溢出,當計數值爲4比特時,溢出的機率爲:1.37 * 10^-15 * m,雖然很低,但對某些應用可能不夠。一個簡單的解決方法是,當計數值到達最大值時,就不在自增,但這致使假陰性。
     
  • 壓縮布隆過濾器(Compressed Bloom Filters)

    在網絡應用中,布隆過濾器一般被做爲信息在各節點間傳送,爲了節約資源,天然而然就想能不能壓縮布隆過濾器後再傳送
    1. 由前面咱們知道,要使得布隆過濾器有最小的假陽性機率,數組中包含的0或1的機率應該是同樣的,根據香農編碼原理(Shannon coding principle),這樣的布隆過濾器不能被壓縮。雖然這樣的布隆過濾器不能被直接壓縮,但咱們能夠用其餘方法達到同樣的效果。
    2. 要使得布隆過濾器 x 與布隆過濾器 y( 包含的0或1的機率應該是同樣的)具備相同的假陽性機率,那麼,x 的大小要大於 y 的,x 的哈希函數的數量不一樣於 y 的,這樣 x 中包含的0和1的數量就不一樣,x 就能夠被壓縮。 
    3. 問題出來了,壓縮布隆過濾器的緣由是更節省空間,咱們找了個更大的布隆過濾器壓縮,那麼壓縮後的布隆過濾器的空間效率比原布隆過濾器更加優秀嗎?是的。
    4. 壓縮後,布隆過濾器的本地存儲空間會變大,但哈希函數數量會變小(更少的映射操做)、傳送的位更少
     
  • D-left 計算布隆過濾器(D-left Counting Bloom Filters)

    上面提到的計算布隆過濾器存在這樣的缺點:存儲空間是標準布隆過濾器的數倍(取決於計數值的位數)和計數值的不均勻(有些始終爲0,有些則可能溢出)。下面看看 D-left Counting Bloom Filters 的特色。D-left Counting Bloom Filters 基於 D-left Hashing。

    D-left Hashing 基本結構網絡

    1. 將一個哈希表分紅幾個不相交的子表(subtable)
    2. 每一個子表裏都有數量相同的桶(bucket)
    3. 每一個桶裏都有必定數量的單元(cell,單元包括特徵值和計數值)
    4. 每一個單元都是固定的位數組成,用來保存元素的特徵值(fingerprint)
    5. 只有一個哈希函數,該哈希函數能夠生成和子表數量相同的桶地址和一個特徵值
     
    插入操做

    假設有 d 個子表,元素爲 x,哈希函數爲 f

    1. 計算 f(x),生成桶地址 addr0, addr1, ..., addr(d-1),特徵值 p 
    2. 咱們檢查子表 i 中地址爲 addri 的桶中的全部單元(i = 0,1,...,d-1)
    3. 若是某個單元中的特徵值和 p 相等,那麼元素 x 就在該哈希表中
    4. 若沒有找到這樣的單元,那麼須要找到存儲特徵值最少的桶(在上面生成的桶地址中找),而後將該特徵值 p 隨機放入該桶的一個空單元中,該單元的計數值變爲1,這考慮了裝載平衡
     

    D-left Counting Bloom Filters數據結構


    由上可知,d-left Hashing 的計數值最大爲零,不支持刪除操做,爲了將它變成可 Counting,可讓它的計數值變成由多位組成。但這樣依然會出現問題,以下:
    app

    1. 假設 d-left counting bloom filter 包含 4 個子表,每一個子表又包含 4 個桶,初始爲空。
    2. 如今有兩個元素 x 和 y 須要映射到過濾器中,f(x) = (1, 1, 1, 1,r), f(y) = (1, 2, 3, 4, r)
    3. 已知插如 x 時,第四個子表的第一個桶最空,x 的特徵值 r 被插入該桶的某一個單元中,該單元計數值變爲1,而插入 y 時,第一個子表的第一個桶最空,y 的特徵值 r 被插入該桶的某一個單元中,該單元計計數值變爲1
    4. 如今要刪除 x,那麼就會尋找每一個子表的第一個桶中的單元,這時,在第一個子表的第一個桶中找到了特徵值 r,接下來就會將該單元的計數值減 1 變爲 0,同時,存儲的特徵值被刪除,變爲空。
    5. 如今查找 x 是否在表中,結果返回真,而查詢 y 是否在表中,結果返回假,致使錯誤。  

     

    爲何會出現上面的狀況?由三個因素促成dom

    1. x 和 y 有相同的特徵值 r 
    2. f(x) 和 f(y) 生成的地址有相同的
    3. x 和 y 特徵值存儲的地方還不同(存同樣就不會出錯) 

     

    如何解決?ide

    說實話,沒看懂英文描述的內容。。。。大體是作了排列置換等操做


    性能分析

    比普通的計算布隆過濾器空間少了一半甚至更多,並且效率也有提高(假陽性更低) 

 

  • Spectral Bloom Filters

    Counting Bloom Filters 能夠進行元素的刪除操做,然而卻不能記錄一個元素被映射的頻率,並且不少應用中元素出現的頻率相差很大,也就是說,CBF中每一個計數值的位數同樣,那麼有些計數值很快就會溢出,而另外一些則一直都很小。這些問題能夠被 Spectral Bloom Filters 解決。
    在SBF中,每個計數值的位數都是動態改變的。它的構造我沒看懂,先留着吧
 
  • Dynamic Counting Filters

    Spectral bloom filter 被提出來解決元素頻率查詢問題,可是,它構造了一個複雜的索引數據結構去解決動態計算器的存儲問題。Dynamic counting bloom filter(比SBF好理解多了) 是一個空間時間都很高效的數據結構,支持元素頻率查詢。相比於SBF,在實際應用中(計數器不是很大,改變不是很頻繁時)它有更快的訪問時間和更小的內存消耗。

    構成部分

    1. DCBF由兩部分組成,第一部分是基礎的計算布隆過濾器
    2. 第二部分是一個一樣大小的向量,用於記錄第一部分中計算器溢出的次數 
    3. 第一部分中的計算器位數固定,第二部分中每一個溢出計算器位數動態改變

     

    特色

    1. 當第二部分溢出計算器也面臨溢出時,會從新申請一個向量,給要溢出部分增長位數,其餘溢出計算器直接拷貝到新的向量中的對應位置,舊的向量會被釋放
 
  • 學習案例

    Summary Cache

        在網絡中有極大的資源請求,若是全部的請求都由服務器來處理,網絡就會出現擁堵,性能就會降低。因此網絡中有大量的中間代理節點。這些代理會把一部分資源放在本身的本地緩存,當用戶向服務器請求資源時,該代理先會檢查該資源是否在本身的緩存中,若是在就直接發送給用戶,不然再向服務器請求。一個代理可以存儲的資源是很是有限的,爲了進一步減輕服務器的負載,網絡中相鄰的代理均可以共享本身的緩存。這樣,當代理 A 本地緩存沒有時,就會向相鄰代理廣播請求,查詢他們是否有該緩存。
        然而,這樣依舊有很大問題,假設,這裏有 N 個代理,每一個代理的命中率爲 H,一個代理平均請求 R 次,那麼廣播中,一個代理收到的查詢信息共有 (N-1) * (1-H) * R 條,總共的請求也就是 
    N * (N-1) * (1-H) * R。這是很是低效的。
        再次改進,各個代理之間交換本身緩存的摘要信息。這樣,當代理 A 失敗後,會先查詢各個代理的摘要信息,而後決定是定向向某個代理請求,仍是向服務器請求資源。這就大大的減小了網絡通訊量。爲了知足快速查詢、更新摘要信息,一個很是好的選擇就是計算布隆過濾器(Counting bloom filters)。

    IP Traceback

       網絡中存在許多攻擊,有時候須要根據一些數據包去還原IP路徑,找到攻擊者。一個可行的辦法是在路由器中存儲數據包信息。然而,有些網絡中通訊量巨大,存儲全部的包是不現實的,所以能夠存儲這些包的摘要信息。這時,選用布隆過濾器能夠極大的節省空間,並且具備很是快的查詢。

     

 

 

  • 代碼實現

標準布隆過濾器構建、測試代碼(Python 面向過程版)

 1 import math
 2 import random
 3 import time
 4 
 5 
 6 def hash_function(a, b, c, item, tablelen):
 7     return (a * item ** 2 + b * item + c) % tablelen  #哈希函數
 8 
 9 
10 def construction_of_SBF(tablelen = 1000, set = []):
11     
12     k = int(math.log(2, math.e) * (tablelen / len(set)))
13     hash = []
14     random.seed(time.time())
15     for i in range(k):  #隨機生成哈希函數的三個參數
16         a = random.randint(1, 1000)
17         b = random.randint(1, 1000)
18         c = random.randint(1, 1000)
19         hash.append((a, b, c))
20 
21     bitArray = [0] * tablelen
22 
23     for element in set:     #映射集合元素到位數組
24         for i in range(k):
25             hx = hash_function(hash[i][0], hash[i][1], hash[i][2], element, tablelen)
26             bitArray[hx] = 1
27 
28     filter = [bitArray, hash]
29     return filter
30     
31 # 測試
32 def test_bloom_filters(bloom_filter = None):
33     if bloom_filter == None:
34         return False
35     
36     testSet = [1, 3, 7, 111, 99, 54, 34, 67, 81, 121, 101, 100, 23, 0, 845, 3339, 44]
37     for item in testSet:
38         flag = True
39         for i in range(len(filter[1])):
40             hx = hash_function(filter[1][i][0], filter[1][i][1], filter[1][i][2], item, len(filter[0]))
41             if bloom_filter[0][hx] != 1:
42                 flag = False 
43                 break
44 
45         if flag is True:
46             print("%d is in filter\n" % item)
47         else:
48             print("%d is not in filter\n" % item)
49     
50     return True
51     
52 
53 if __name__ == "__main__":
54     filter = construction_of_SBF(set = list(range(10)))
55     test_bloom_filters(filter)
View Code

 

計算布隆過濾器構建、測試代碼(Python 面向過程版)

  1 import math
  2 import random
  3 import time
  4 
  5 """
  6 結構沒有設置好,按下寫:
  7 0. 封裝函數
  8 1. 哈希函數:計算哈希值
  9 2. 生成哈希隨機參數函數
 10 3. 插入函數:被調用
 11 4. 刪除函數:被調用
 12 5. 查詢函數:測試函數調用
 13 6. 測試函數:測試插入和刪除
 14 
 15 """
 16 
 17 
 18 def hash_function(params, item, tlen):
 19     return (params[0] * item ** 2 + params[1] * item + params[2]) % tlen
 20 
 21 
 22 def deletion_counting_bloom_filter(cbfilter = None, item = None):
 23     if (cbfilter is None) or (item is None):
 24         return False
 25     for params in cbfilter[2]:
 26         cbfilter[0][hash_function(params, item, len(cbfilter[0]))] -= 1
 27     return True
 28 
 29 
 30 def insertion_counting_bloom_filter(item = None, cbfilter = None):
 31     if (item == None) or (cbfilter == None):
 32         return False 
 33     for params in cbfilter[2]:
 34         cbfilter[0][hash_function(params, item, len(cbfilter[0]))] += 1
 35     return True
 36 
 37 
 38 def query_counting_bloom_filter(item = None, cbfilter = None):
 39     for params in cbfilter[2]:
 40         if(cbfilter[0][hash_function(params, item, len(cbfilter[0]))]) is 0:
 41             return False
 42     return True
 43 
 44 
 45 def construction_counting_bloom_filter(filterSet = None, filterArray = None):
 46     if (filterSet is None) or (filterArray is None):
 47         return None
 48     # 最佳的哈希函數數量
 49     hashNum = int(math.log(2, math.e) * (len(filterArray) / len(filterSet)))
 50     hashParam = []
 51     random.seed(time.time())
 52     # 隨機生成哈希參數
 53     for i in range(hashNum):
 54         a = random.randint(1, 9999)
 55         b = random.randint(1, 9999)
 56         c = random.randint(1, 9999)
 57         hashParam.append((a, b, c))
 58     
 59     # 將初始集合元素映射到過濾器數組中
 60     for item in filterSet:
 61         for params in hashParam:
 62             filterArray[hash_function(params, item, len(filterArray))] += 1
 63 
 64     # 返回過濾器數組、過濾器集合、過濾器哈希參數
 65     return (filterArray, filterSet, hashParam)
 66 
 67 
 68 def test_counting_bloom_filters(cbfilter = None):
 69     if cbfilter is None:
 70         return None
 71     testSet = cbfilter[1][10:20]
 72     
 73     # 先測試原有元素是否正常映射
 74     for item in testSet:
 75         if query_counting_bloom_filter(item, cbfilter) is True:
 76             print("%d is in filter\n" % item)
 77         else:
 78             print("%d is not in filter\n" % item)
 79 
 80     # 刪除後再查詢
 81     if deletion_counting_bloom_filter(cbfilter, testSet[0]) is True:
 82         print("delete successfully!\n")
 83     else :
 84         print("delete fails\n")
 85 
 86     if query_counting_bloom_filter(testSet[0], cbfilter) is True:
 87         print("%d is in filter\n" % testSet[0])
 88     else :
 89         print("%d is not in filter\n" % testSet[0])
 90 
 91     # 插入後再測試
 92     if insertion_counting_bloom_filter(testSet[0], cbfilter) is True:
 93         print("insert %d successfully\n" % testSet[0])
 94     else:
 95         print("insert %d fails\n")
 96     
 97     if query_counting_bloom_filter(testSet[0], cbfilter) is True:
 98         print("%d is in filter\n" % testSet[0])
 99     else :
100         print("%d is not in filter\n" % testSet[0])
101 
102 
103 # 封裝後的函數
104 def counting_bloom_filters(filterSet = None, filterArray = None):
105     if (filterSet is None) or (filterArray is None):
106         return False
107     # 構造:初始集合元素的映射、哈希函數參數生成
108     cbfilter = construction_counting_bloom_filter(filterSet, filterArray)
109 
110     # 測試:測試插入、刪除、查詢
111     test_counting_bloom_filters(cbfilter)
112 
113 
114 if __name__ == "__main__":
115     filterSet = list(range(100))
116     filterArray = [0] * 10000
117     counting_bloom_filters(filterSet, filterArray)
View Code

 

標準布隆過濾器構建、測試代碼(Java 面向對象版)

  1 // package BloomFilters;
  2 
  3 import java.util.Arrays;
  4 import java.util.Random;
  5 import java.io.*;
  6 import java.math.BigInteger;
  7 import java.nio.*;
  8 import java.nio.charset.StandardCharsets;
  9 import java.nio.file.Path;
 10 import java.util.*;
 11 
 12 /**
 13  * 實現標準布隆過濾器的類
 14  */
 15 public class SBFilters {
 16     // 實例字段
 17     private boolean[] bitArray; //位數組
 18     private int[][] hashParams; //隨機的哈希函數參數
 19 
 20     // 方法字段
 21     public SBFilters(int tLen, int[] iSet)
 22     {
 23         this.bitArray = new boolean[tLen];
 24         Arrays.fill(this.bitArray, Boolean.FALSE);
 25         this.construction_filter(iSet);
 26     }
 27 
 28     private boolean construction_filter(int[] iSet)
 29     {
 30         if(iSet == null || iSet.length == 0)
 31         {
 32             return false;
 33         }
 34         var hashNum = (int)(Math.log(2) * (this.bitArray.length / iSet.length));
 35         this.construction_hashParams(hashNum);
 36         for(var item: iSet)
 37         {
 38             for(var params: this.hashParams)
 39             {
 40                 this.bitArray[hash_function(params, item)] = true;
 41             }
 42         }
 43         return true;
 44     }
 45 
 46     private boolean construction_hashParams(int hashNum)
 47     {
 48         this.hashParams = new int[hashNum][3];
 49         var time = System.currentTimeMillis();
 50         var rd = new Random(time);
 51         for(int i = 0; i < hashNum; i++)
 52         {
 53             this.hashParams[i][0] = rd.nextInt(9999) + 1;
 54             this.hashParams[i][1] = rd.nextInt(9999) + 1;
 55             this.hashParams[i][2] = rd.nextInt(9999) + 1;
 56         }
 57         return true;
 58     }
 59 
 60     private int hash_function(int[] params, int item)
 61     {
 62         return (int)((params[0] * Math.pow(item, 2.0) + 
 63             params[1] * item + params[2]) % bitArray.length);
 64     }
 65 
 66     public boolean query_filter(int item)
 67     {
 68         for(var params: this.hashParams)
 69         {
 70             if(this.bitArray[hash_function(params, item)] == false)
 71             {
 72                 return false;
 73             }
 74         }
 75         return true;
 76     }
 77     
 78 }
 79 
 80 
 81 
 82 // package BloomFilters;
 83 
 84 
 85 
 86 
 87 /**
 88  * 用來測試實現的布隆過濾器是否正常工做
 89  */
 90 public class FiltersTest
 91 {  
 92     public static void main(final String[] args) 
 93     {
 94         test_counting_bloom_filters();
 95     }
 96 
 97 
 98     private static void test_counting_bloom_filters()
 99     {
100         var iSet = new int[10000];
101         for(int i = 0; i < 10000; iSet[i] = i++);
102         SBFilters sbFilter = new SBFilters(999999, iSet);
103        
104         for(var item: new int[]{1, 3, 5, 78, 99, 100, 101, 9999, 10000, 3534})
105         {
106             var isIn = sbFilter.query_filter(item);
107             if(isIn == false)
108             {
109                 System.out.printf("%d is not in the filter\n", item);
110             }
111             else
112             {
113                 System.out.printf("%d is in the filter\n", item);
114             }
115         }
116     }
117 
118     
119 }
View Code
相關文章
相關標籤/搜索