大量數據去重:Bitmap和布隆過濾器(Bloom Filter)

5TB的硬盤上放滿了數據,請寫一個算法將這些數據進行排重。若是這些數據是一些32bit大小的數據該如何解決?若是是64bit的呢?java

在面試時遇到的問題,問題的解決方案十分典型,但對於海量數據處理接觸少的同窗可能一時也想不到什麼好方案。介紹兩個算法,對於空間的利用到達了一種極致,那就是Bitmap和布隆過濾器(Bloom Filter)面試

Bitmap算法

在網上並無找到Bitmap算法的中文翻譯,在《編程珠璣》中有說起。與其說是算法,不如說是一種緊湊的數據存儲結構。其實若是並不是如此大量的數據,有不少排重方案可使用,典型的就是哈希表算法

public int[] removeDuplicates(int[] array) {
    int index = 0;
    Map<Integer, Boolean> maps = new LinkedHashMap<Integer, Boolean>();
    for(int num : array) {
        if(!maps.contains(num)) {
            array[index] = num;
            index++;
            maps.put(num, true);
        }
    }

    return newArray;
}

實際上,哈希表實際上爲每個可能出現的數字提供了一個一一映射的關係,每一個元素都至關於有了本身的獨享的一份空間,這個映射由散列函數來提供(這裏咱們先不考慮碰撞)。實際上哈希表甚至還能記錄每一個元素出現的次數,這樣的數據結構完成這個任務有點「大材小用」了。編程

咱們拆解一下咱們的需求:數組

  1. 集合中每一個元素(示例中是int)有一個獨享的空間
  2. 找到一個到這個空間的映射方法

這個空間要多大?對於咱們的問題來講,一個boolean就夠了,或者說,1個bit就夠了,咱們只想知道某個元素出現過沒有。若是爲每一個全部可能的值分配1個bit,32bit的int全部可能取值須要內存空間爲:數據結構

 

2 32 bit=2 29 Byte=512MB 232bit=229Byte=512MBdom

 

那怎麼樣完成這個映射呢?其實就是Bitmap所要完成的工做了。若是咱們把整型0x0一、0x0二、…、0x08的空間依次映射到一個Byte上,每一個bit就表明這個int值是否出現過,初值爲0(false)。ide

若擴展到整個int取值域,申請一個byte[]便可,示例代碼以下:函數

public static final int _1MB = 1024 * 1024;

public static byte[] flags = new byte[ 512 * _1MB ];


public static void main(String[] args) {

    int[] array = {255, 1024, 0, 65536}

    int index = 0;
    for(int num : array) {
        if(!getFlags(num)) {
            //未出現的元素
            array[index] = num;
            index = index + 1;
            //設置標誌位
            setFlags(num);
        }
    }
}

public static void setFlags(int num) {
    flags[num >> 3] |= 0x01 << (num & (0x07));
}

public static boolean getFlags(int num) {
    return flags[num >> 3] >> (num & (0x07)) & 0x01;
}

其實,就是按int從小到大的順序依次擺放到byte[]中,僅涉及到一些除以2的整次冪和對2的整次冪取餘的位操做小技巧。很顯然,對於小數據量、數據取值很稀疏,上面的方法並無什麼優點,但對於海量的、取值分佈很均勻的集合進行去重,Bitmap極大地壓縮了所須要的內存空間。於此同時,還額外地完成了對原始數組的排序工做。缺點是,Bitmap對於每一個元素只能記錄1bit信息,若是還想完成額外的功能,恐怕只能靠犧牲更多的空間、時間來完成了。性能

布隆過濾器(Bloom Filter)

然而Bitmap不是萬能的,若是數據量大到必定程度,如開頭寫的64bit類型的數據,還能不能用Bitmap?咱們來算一算:

 

2 64 bit=2 61 Byte=2048PB=2EB 264bit=261Byte=2048PB=2EB

 

EB(Exabyte,艾字節)這個計算機科學中統計數據量的單位有多大,有興趣的小夥伴能夠查閱下資料。這個量級的Bitmap,已經不是人類硬件所能承擔的了。我相信誰也不會想用集羣去計算這麼一個問題吧?因此Bitmap的好處在於空間複雜度不隨原始集合內元素的個數增長而增長,而它的壞處也源於這一點——空間複雜度隨集合內最大元素增大而線性增大

因此接下來,咱們要引入另外一個著名的工業實現——布隆過濾器(Bloom Filter)。若是說Bitmap對於每個可能的整型值,經過直接尋址的方式進行映射,至關於使用了一個哈希函數,那布隆過濾器就是引入了k(k>1) k(k>1) 個相互獨立的哈希函數,保證在給定的空間、誤判率下,完成元素判重的過程。下圖中是k=3 k=3 時的布隆過濾器。
圖1 布隆過濾器(來源:wiki)

x,y,z x,y,z 經由哈希函數映射將各自在Bitmap中的3個位置置爲1,當w w 出現時,僅當3個標誌位都爲1時,才表示w w 在集合中。圖中所示的狀況,布隆過濾器將斷定w w 不在集合中。

那麼布隆過濾器的偏差有多少?咱們假設全部哈希函數散列足夠均勻,散列後落到Bitmap每一個位置的機率均等。Bitmap的大小爲m m 、原始數集大小爲n n 、哈希函數個數爲k k :

  1. 1個散列函數時,接收一個元素時Bitmap中某一位置爲0的機率爲:

    1−1m  1−1m

  2. k k 個相互獨立的散列函數,接收一個元素時Bitmap中某一位置爲0的機率爲:

    (1−1m ) k  (1−1m)k

  3. 假設原始集合中,全部元素都不相等(最嚴格的狀況),將全部元素都輸入布隆過濾器,此時某一位置仍爲0的機率爲:

    (1−1m ) nk  (1−1m)nk


    某一位置爲1的機率爲:

    1−(1−1m ) nk  1−(1−1m)nk

  4. 當咱們對某個元素進行判重時,誤判即這個元素對應的k k 個標誌位不全爲1,但全部k k 個標誌位都被置爲1,誤判率ε ε 爲:

    ε≈[1−(1−1m ) nk ] k  ε≈[1−(1−1m)nk]k


    這個誤判率應當比實際值大,由於將判斷正確的狀況也算進去了。根據著名極限lim n→∞ (1+1n ) n =e limn→∞(1+1n)n=e 能夠獲得:

    ε≈[1−e −nkm  ] k  ε≈[1−e−nkm]k


    ε ε 獲得最優解1,當且僅當:

    k=mn ln2≈0.7mn  k=mnln⁡2≈0.7mn


    此時,誤判率ε ε 與數集大小和

    ε≈(1−e −ln2 ) ln2mn  =0.5 ln2mn  =0.5 k  ε≈(1−e−ln⁡2)ln2mn=0.5ln2mn=0.5k

回到咱們的問題中,有趣的是因爲硬盤空間是限制死的,集合元素個數n n 的大小反而與單個數據的比特數成反比,數據長度爲64bit時,

 

n=5TB64bit =5×2 40 Byte8Byte ≈2 34  n=5TB64bit=5×240Byte8Byte≈234

 

若以m=16n m=16n 計算,Bitmap集合的大小爲2 38 bit=2 35 Byte=32GB 238bit=235Byte=32GB ,此時的ε≈0.0005 ε≈0.0005 。而且要知道,以上計算的都是偏差的上限

布隆過濾器經過引入必定錯誤率,使得海量數據判重在能夠接受的內存代價中得以實現。從上面的公式能夠看出,隨着集合中的元素不斷輸入過濾器中(n n 增大),偏差將愈來愈大。可是,當Bitmap的大小m m (指bit數)足夠大時,好比比全部可能出現的不重複元素個數還要大10倍以上時,錯誤機率是能夠接受的。

最後咱們所要作的,就是實現一個布隆過濾器,而後利用它對硬盤上的5TB數據一一判重,並寫回硬盤中。這其中可能涉及到利用讀寫的buffer,待有時間補上。

附錄

這裏有一個google實現的布隆過濾器,咱們來看看它的誤判率:

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.util.HashSet;
import java.util.Random;

public class testBloomFilter {

    static int sizeOfNumberSet = Integer.MAX_VALUE >> 4;

    static Random generator = new Random();

    public static void main(String[] args) {

        int error = 0;
        HashSet<Integer> hashSet = new HashSet<Integer>();
        BloomFilter<Integer> filter = BloomFilter.create(Funnels.integerFunnel(), sizeOfNumberSet);

        for(int i = 0; i < sizeOfNumberSet; i++) {
            int number = generator.nextInt();
            if(filter.mightContain(number) != hashSet.contains(number)) {
                error++;
            }
            filter.put(number);
            hashSet.add(number);
        }

        System.out.println("Error count: " + error + ", error rate = " + String.format("%f", (float)error/(float)sizeOfNumberSet));
    }
}

在這個實現中,Bitmap的集合m m 、輸入的原始數集合n n 、哈希函數k k 的取值都是按照上面最優的方案選取的,默認狀況下保證誤判率ε=0.5 k <0.03≈0.5 5  ε=0.5k<0.03≈0.55 ,於是此時k=5 k=5 。

/**
 * Creates a {@link BloomFilter BloomFilter<T>} with the expected number of
 * insertions and a default expected false positive probability of 3%.
 */
public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
    return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}

而還有一個頗有趣的地方是,實際使用的卻並非5個哈希函數。實際進行映射時,而是分別使用了一個64bit哈希函數的高、低32bit進行循環移位。註釋中包含着這個算法的論文「Less Hashing, Same Performance: Building a Better Bloom Filter」,論文中指明其對過濾器性能沒有明顯影響。很明顯這個實現對於m>2 32  m>232 時的支持並很差,由於當大於2 31 −1 231−1 的下標在算法中並不能被映射到。

enum BloomFilterStrategies implements BloomFilter.Strategy {
  /**
   * See "Less Hashing, Same Performance: Building a Better Bloom Filter" by Adam Kirsch and
   * Michael Mitzenmacher. The paper argues that this trick doesn't significantly deteriorate the
   * performance of a Bloom filter (yet only needs two 32bit hash functions).
   */
  MURMUR128_MITZ_32() {
    @Override public <T> boolean put(T object, Funnel<? super T> funnel,
        int numHashFunctions, BitArray bits) {
      long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
      int hash1 = (int) hash64;
      int hash2 = (int) (hash64 >>> 32);
      boolean bitsChanged = false;
      for (int i = 1; i <= numHashFunctions; i++) {
        int nextHash = hash1 + i * hash2;
        if (nextHash < 0) {
          nextHash = ~nextHash;
        }
        bitsChanged |= bits.set(nextHash % bits.bitSize());
      }
      return bitsChanged;
    }

    @Override public <T> boolean mightContain(T object, Funnel<? super T> funnel,
        int numHashFunctions, BitArray bits) {
      long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
      int hash1 = (int) hash64;
      int hash2 = (int) (hash64 >>> 32);
      for (int i = 1; i <= numHashFunctions; i++) {
        int nextHash = hash1 + i * hash2;
        if (nextHash < 0) {
          nextHash = ~nextHash;
        }
        if (!bits.get(nextHash % bits.bitSize())) {
          return false;
        }
      }
      return true;
    }
  };
  ...
}
  1. WikiPedia - Bloom Filter
相關文章
相關標籤/搜索