算法分析:使用布隆過濾器(Bloom Filter)進行大數據量排序

      題目大意:移動公司須要對已經發放的全部139段的號碼進行統計排序,已經發放的139號碼段的文件都存放在一個文本文件中(原題是放在兩個文件中),一個號碼一行,如今須要將文件裏的全部號碼進行排序,並寫入到一個新的文件中;號碼可能會有不少,最多可能有一億個不一樣的號碼(全部的139段號碼),存入文本文件中大概要佔1.2G的空間;JVM最大的內存在300之內,程序要考慮程序的可執行性及效率;只能使用Java標準庫,不得使用第三方工具。
      這是個典型的大數據量的排序算法問題,首先要考慮空間問題,一下把.2G的數據讀入內存是不太可能的,就算把壹億條數據都轉換成INT類型存儲也要佔接近400M的空間。當時作個題目我並無想太多的執行效率問題,主要就考慮了空間,並且習慣性的想到合併排序,基本思想是原文件分割成若干個小文件並排序,再將排序好的小文件合併獲得最後結果,算法大概以下:
一、順序讀取存放號碼文件的中全部號碼,並取139以後的八位轉換爲int類型;每讀取號碼數滿一百萬個(這個數據可配置)將已經讀取的號碼排序並存入新建的臨時文件。
二、將全部生成的號碼有序的臨時文件合併存入結果文件。
這個算法雖然解決了空間問題,可是運行效率極低,因爲IO讀寫操做太多,加上步驟1中的排序的算法(快速排序)原本效率就不高(對於電話排序這種特殊狀況來講),致使1億條數據排序運行3個小時纔有結果。
如何可以減小排序的時間呢?首當其衝的是減小IO操做,另外若是可以有更加好排序算法也行。前天無聊再看這個題目時忽然想到大三時看《編程珠璣》時上面也有個問題的需求這個這個題目差很少,記得好像使用是位向量(實際上就是一個bit數組),用電話做爲index,心中大喜,找到了解決此問題的最完美方案:用位向量存儲電話號碼,壹個號碼佔壹個bit,壹億個電話號碼也只須要大概12M的空間;算法大概以下:
一、初始化bits[capacity];
二、順序全部讀入電話號碼,並轉換爲int類型,修改位向量值:bits[phoneNum]=1;
三、遍歷bits數組,若是bits[index]=1,轉換index爲電話號碼輸出。
因爲Java中沒有 bit 類型,一個 boolean 值佔空間爲 1byte(感興趣的能夠本身寫程序驗證),我本身寫了個用 int 模擬 bit 數組的類,代碼以下:
public class BitArray {
	private int[] bits = null;
	private int length;
	//用於設置或者提取int類型的數據的某一位(bit)的值時使用
	private final static int[] bitValue = {
		0x80000000,//10000000 00000000 00000000 00000000
		0x40000000,//01000000 00000000 00000000 00000000
		0x20000000,//00100000 00000000 00000000 00000000
		0x10000000,//00010000 00000000 00000000 00000000
		0x08000000,//00001000 00000000 00000000 00000000
		0x04000000,//00000100 00000000 00000000 00000000
		0x02000000,//00000010 00000000 00000000 00000000
		0x01000000,//00000001 00000000 00000000 00000000
		0x00800000,//00000000 10000000 00000000 00000000
		0x00400000,//00000000 01000000 00000000 00000000
		0x00200000,//00000000 00100000 00000000 00000000
		0x00100000,//00000000 00010000 00000000 00000000
		0x00080000,//00000000 00001000 00000000 00000000
		0x00040000,//00000000 00000100 00000000 00000000
		0x00020000,//00000000 00000010 00000000 00000000
		0x00010000,//00000000 00000001 00000000 00000000
		0x00008000,//00000000 00000000 10000000 00000000
		0x00004000,//00000000 00000000 01000000 00000000
		0x00002000,//00000000 00000000 00100000 00000000
		0x00001000,//00000000 00000000 00010000 00000000
		0x00000800,//00000000 00000000 00001000 00000000
		0x00000400,//00000000 00000000 00000100 00000000
		0x00000200,//00000000 00000000 00000010 00000000
		0x00000100,//00000000 00000000 00000001 00000000
		0x00000080,//00000000 00000000 00000000 10000000
		0x00000040,//00000000 00000000 00000000 01000000
		0x00000020,//00000000 00000000 00000000 00100000
		0x00000010,//00000000 00000000 00000000 00010000
		0x00000008,//00000000 00000000 00000000 00001000
		0x00000004,//00000000 00000000 00000000 00000100
		0x00000002,//00000000 00000000 00000000 00000010
		0x00000001 //00000000 00000000 00000000	00000001
	};
	public BitArray(int length) {
		if(length < 0){
			throw new IllegalArgumentException("length必須大於零!");
		}
		bits = new int[length / 32 + (length % 32 > 0 ? 1 : 0)];
		this.length = length;
	}
	//取index位的值
	public int getBit(int index){
		if(index <0 || index > length){
			throw new IllegalArgumentException("length必須大於零小於" + length);
		}
		int intData = bits[index/32];
		return (intData & bitValue[index%32]) >>> (32 - index%32 -1);
	}
	//設置index位的值,只能爲0或者1
	public void setBit(int index,int value){
		if(index <0 || index > length){
			throw new IllegalArgumentException("length必須大於零小於" + length);
		}		
		if(value!=1&&value!=0){
			throw new IllegalArgumentException("value必須爲0或者1");
		}
		int intData = bits[index/32];
		if(value == 1){
			bits[index/32] = intData | bitValue[index%32];
		}else{
			bits[index/32] = intData & ~bitValue[index%32];
		}
	}
	public int getLength(){
		return length;
	}	
}

bit 數組有了,剩下就是算法代碼,核心代碼以下: html

bitArray = new BitArray(100000000);
//順序讀取全部的手機號碼
while((phoneNum = bufferedReader.readLine())!=null){
	//取139號碼的最後8位數字
	phoneNum = phoneNum.trim().substring(3);
	//將後8位轉換爲int類型
	phoneNumAsInt = Integer.valueOf(phoneNum);
	//設置對應bit值爲1
	bitArray.setBit(phoneNumAsInt, 1);
}
//遍歷bit數組輸出全部存在的號碼
for(int i = 0;i<sortUnit;i++){
	if(bitArray.getBit(i)==1){
		writer.write("139" + leftPad(String.valueOf(i + sortUnit*times), 8));
		writer.newLine();
	}
}
writer.flush();
經測試,修改後的算法排序時只須要20多兆的內存,壹億條電話號碼排序只要10分鐘(時間主要花在IO上),看來效果仍是很明顯的。

這個算法很快,不過也有他的侷限性: java

一、只能用於整數的排序,或者能夠準確映射到正整數(對象不一樣對應的正整數也不相同)的數據的排序。
二、不能處理重複的數據,重複的數據排序後只有一條,若是有這種需求,能夠在這個算法的基礎上修改,給出現次數大於1的數據添加個計數器便可,而後存入Map中。
三、對於數據量極其大的數據處理可能仍是比較佔用空間,這種狀況可配合多通道排序算法解決。
這個算法的思想源於《 編程珠璣》中的 布隆過濾器(Bloom Filter),有興趣的同窗能夠讀讀那本書,很是不錯! http://book.douban.com/subject/1230206/

布隆過濾器(Bloom Filter)

在平常生活中,包括在設計計算機軟件時,咱們常常要判斷一個元素是否在一個集合中。好比在字處理軟件中,須要檢查一個英語單詞是否拼寫正確(也就是要判斷它 是否在已知的字典中);在 FBI,一個嫌疑人的名字是否已經在嫌疑名單上;在網絡爬蟲裏,一個網址是否被訪問過等等。最直接的方法就是將集合中所有的元素存在計算機中,遇到一個新 元素時,將它和集合中的元素直接比較便可。通常來說,計算機中的集合是用哈希表(hash table)來存儲的。它的好處是快速準確,缺點是費存儲空間。當集合比較小時,這個問題不顯著,可是當集合巨大時,哈希表存儲效率低的問題就顯現出來了。好比說,壹個像 Yahoo,Hotmail 和 Gmai 那樣的公衆電子郵件(email)提供商,老是須要過濾來自發送垃圾郵件的人(spamer)的垃圾郵件。一個辦法就是記錄下那些發垃圾郵件的 email 地址。因爲那些發送者不停地在註冊新的地址,全世界少說也有幾十億個發垃圾郵件的地址,將他們都存起來則須要大量的網絡服務器。若是用哈希表,每存儲一億 個 email 地址, 就須要 1.6GB 的內存(用哈希表實現的具體辦法是將每個 email 地址對應成一個八字節的信息指紋, 而後將這些信息指紋存入哈希表,因爲哈希表的存儲效率通常只有 50%,所以一個 email 地址須要佔用十六個字節。一億個地址大約要 1.6GB, 即十六億字節的內存)。所以存貯幾十億個郵件地址可能須要上百 GB 的內存。除非是超級計算機,通常服務器是沒法存儲的。 算法

今天,咱們介紹一種稱做布隆過濾器的數學工具,它只須要哈希表 1/8 到 1/4 的大小就能解決一樣的問題。 shell

布隆過濾器是由巴頓*布隆於1970年提出的。它其實是一個很長的二進制向量和一系列隨機映射函數。咱們經過上面的例子來講明起工做原理。
假定咱們存儲一億個電子郵件地址,咱們先創建一個十六億二進制(比特),即兩億字節的向量,而後將這十六億個二進制所有設置爲零。對於每個電子郵件地址 X,咱們用八個不一樣的隨機數產生器(F1,F2, ...,F8) 產生八個信息指紋(f1, f2, ..., f8)。再用一個隨機數產生器 G 把這八個信息指紋映射到 1 到十六億中的八個天然數 g1, g2, ...,g8。如今咱們把這八個位置的二進制所有設置爲一。當咱們對這一億個 email 地址都進行這樣的處理後。一個針對這些 email 地址的布隆過濾器就建成了。詳見下圖: 編程

  如今,讓咱們看看如何用布隆過濾器來檢測一個可疑的電子郵件地址 Y 是否在黑名單中。咱們用相同的八個隨機數產生器(F1, F2, ..., F8)對這個地址產生八個信息指紋 s1,s2,...,s8,而後將這八個指紋對應到布隆過濾器的八個二進制位,分別是 t1,t2,...,t8。若是 Y 在黑名單中,顯然,t1,t2,..,t8 對應的八個二進制必定是一。這樣在遇到任何在黑名單中的電子郵件地址,咱們都能準確地發現。
  布隆過濾器決不會漏掉任何一個在黑名單中的可疑地址。可是,它有一條不足之處。也就是它有極小的可能將一個不在黑名單中的電子郵件地址斷定爲在黑名單中,由於有可能某個合理使用的郵件地址正巧對應個八個都被設置成一的二進制位。好在這種可能性很小。咱們把它稱爲誤識機率。在上面的例子中,誤識機率在萬分之一如下。
布隆過濾器的好處在於快速,省空間。可是有必定的誤識別率。常見的補救辦法是在創建一個小的白名單,存儲那些可能別誤判的郵件地址。 數組

相關文章
相關標籤/搜索