算法介紹 Bloom Filter的中文名稱叫做布隆過濾器,因為他最早的提出者叫做布隆(Bloom),因而而得此名。布隆過濾器簡單的說就是為了檢索一個元素是否存在于某個集合當(dāng)中,以此實(shí)現(xiàn)數(shù)據(jù)的過濾。也許你會想,這還不簡單,判斷元素是否存在某集合中,遍歷集合,
算法介紹
Bloom Filter的中文名稱叫做布隆過濾器,因為他最早的提出者叫做布隆(Bloom),因而而得此名。布隆過濾器簡單的說就是為了檢索一個元素是否存在于某個集合當(dāng)中,以此實(shí)現(xiàn)數(shù)據(jù)的過濾。也許你會想,這還不簡單,判斷元素是否存在某集合中,遍歷集合,一個個去比較不就能得出結(jié)果,當(dāng)然這沒有任何的問題,但是當(dāng)你面對的是海量數(shù)據(jù)的時候,在空間和時間上的代價是非常恐怖的,顯然需要更好的辦法來解決這個問題,而Bloom Filter就是一個不錯的算法。具體怎么實(shí)現(xiàn),接著往下看。
先來說說傳統(tǒng)的元素檢索的辦法,比如說事先在內(nèi)存中存儲了一堆的url字符數(shù)組,然后給定一個指定的url,判斷是否存在于之前的集合中,我們肯定是加載整個數(shù)組到內(nèi)存中,然后一個個去比較,假設(shè)每條url字符平均所占的量只有幾個字節(jié),但是當(dāng)數(shù)據(jù)變?yōu)楹A康臅r候,也足以撐爆整個內(nèi)存,這是空間上的一個局限。再者,逐次遍歷的方式本身就是一種暴力搜索,搜索的時間將會隨著集合容量的本身而線性擴(kuò)展,一旦數(shù)據(jù)量變大,查詢時間上的開銷也是非常恐怖的。針對時間和空間上的問題,Bloom Filter都給出了完美的解決辦法。首先第一個空間的問題,原本的數(shù)據(jù)占用的是字符,在這里我們用1個位占據(jù),也就是說1個元素我用1/8的字節(jié)表示,不管你的url長度是10個字符,100字符,統(tǒng)統(tǒng)用一個位表示,所以在這里我們需要能夠保證每個字符所代表的位不能沖突。因為用到了位的存儲,我們需要對數(shù)據(jù)進(jìn)行一個hash映射,以此得到他的位置,然后將此位置上的位置標(biāo)為1(默認(rèn)都是為0)。所以說白了,Bloom Filter就是由一個很長的位數(shù)組和一些隨機(jī)的哈希函數(shù)構(gòu)成。位數(shù)組你可以想象成下面的這種形式:
你可以想象這個長度非常長,反正1個單位就占據(jù)1個位,1k的空間就已經(jīng)能夠表示1024*8=8192位了。所以說內(nèi)存空間得到了巨大的節(jié)約。現(xiàn)在一個問題來了,為什么我剛剛用了一些隨機(jī)的哈希函數(shù)這個詞而不是說一個呢,因為會有哈希碰撞,再好的哈希函數(shù)也不能保證不會發(fā)生哈希沖突,所以這里需要采用多個哈希函數(shù),所以元素是否存在的判斷條件就變?yōu)榱酥挥兴械墓:瘮?shù)映射的位置的值都是true的情況下,此元素才是存在于集合中的,這樣判斷的準(zhǔn)確率就會大大提升了,哈希映射之后的效果圖如下:
假設(shè)我們的程序采用了如上圖所示的3個隨機(jī)獨(dú)立的哈希函數(shù),1個元素需要進(jìn)行3次不同的哈希函數(shù)的映射算法,對3個位置進(jìn)行標(biāo)記,對此元素的誤判概率我們做個計算,要使此元素誤判,就是說,他的這3個位置都有人占據(jù)了,就是說都與別的哈希函數(shù)有沖突,這最糟糕的情況就是他的3個映射位置與某個其他的元素通過哈希函數(shù)計算完全重疊,假設(shè)位空間長度1W位。每個位置被映射的概率就為1/1w,所以最糟糕的情況的沖突概率就是1/1w*1/1w*1/1w=1/10的12次方,如果最大的沖突概率的可能性呢,就是每個位置都與其中的某個哈希函數(shù)映射沖突,那誤差概率就是疊加的情況1/1w+1/1w+1/1w=0.0003。結(jié)果已經(jīng)非常明顯了,通過3個哈希函數(shù)就已經(jīng)能夠保證足夠低的誤判率了,更別說當(dāng)你用4個,5個哈希函數(shù)做映射的情況。下面問題又轉(zhuǎn)移到了我們用什么方式去作為位數(shù)組呢,int數(shù)組,字符char數(shù)組,答案都不是。結(jié)果在下面。
這個是java中的某個數(shù)據(jù)類型,C,C++我目前不清楚有沒有這樣的類,為什么選用這個而不是前面說的int,或char數(shù)組,首先int當(dāng)然不行,1個int本身就有32位,占了4個字節(jié),用他做出0,1的存儲顯然相當(dāng)于沒省下空間,自然我們就想到了用字符數(shù)組char[],在C語言中1個char占一個字節(jié),而在java中由于編碼方式的不同,一個char占2個字節(jié),用char做存儲也只是稍稍比int介紹了一半的空間,并沒有真正的做到一個元素用一個位來表示,后來查了一下,java里面就有內(nèi)置了BitSet專門就是做位存儲的,還能夠進(jìn)行位相關(guān)的許多操作,他的操作其實(shí)就是和數(shù)組一樣,也是從0開始的。不熟悉的同學(xué)可以自行上網(wǎng)查閱相關(guān)資料,其實(shí)int數(shù)組也可以實(shí)現(xiàn)類似的功能,不過自己要做轉(zhuǎn)換,把int當(dāng)成32位來算,之前我寫過相關(guān)的文章,是關(guān)于位示圖法存儲大數(shù)據(jù)。
算法其實(shí)非常的簡單,我這里用一組少量的數(shù)據(jù)進(jìn)行模擬。
輸入數(shù)據(jù)input.txt:
mike study day get last exam think fish he
play mike study day get Axis last exam think fish he
算法的工具類BloomFilterTool.java:
package BloomFilter; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.BitSet; import java.util.HashMap; import java.util.Map; /** * 布隆過濾器算法工具類 * * @author lyq * */ public class BloomFilterTool { // 位數(shù)組設(shè)置為10w位的長度 public static final int BIT_ARRAY_LENGTH = 100000; // 原始文檔地址 private String filePath; // 測試文檔地址 private String testFilePath; // 用于存儲的位數(shù)組,一個單元用1個位存儲 private BitSet bitStore; // 原始數(shù)據(jù) private ArrayListtotalDatas; // 測試的查詢數(shù)據(jù) private ArrayList queryDatas; public BloomFilterTool(String filePath, String testFilePath) { this.filePath = filePath; this.testFilePath = testFilePath; this.totalDatas = readDataFile(this.filePath); this.queryDatas = readDataFile(this.testFilePath); } /** * 從文件中讀取數(shù)據(jù) */ public ArrayList readDataFile(String path) { File file = new File(path); ArrayList dataArray = new ArrayList (); try { BufferedReader in = new BufferedReader(new FileReader(file)); String str; String[] tempArray; while ((str = in.readLine()) != null) { tempArray = str.split(" "); for(String word: tempArray){ dataArray.add(word); } } in.close(); } catch (IOException e) { e.getStackTrace(); } return dataArray; } /** * 獲取查詢總數(shù)據(jù) * @return */ public ArrayList getQueryDatas(){ return this.queryDatas; } /** * 用位存儲數(shù)據(jù) */ private void bitStoreData() { long hashcode = 0; bitStore = new BitSet(BIT_ARRAY_LENGTH); for (String word : totalDatas) { // 對每個詞進(jìn)行3次哈希求值,減少哈希沖突的概率 hashcode = BKDRHash(word); hashcode %= BIT_ARRAY_LENGTH; bitStore.set((int) hashcode, true); hashcode = SDBMHash(word); hashcode %= BIT_ARRAY_LENGTH; bitStore.set((int) hashcode, true); hashcode = DJBHash(word); hashcode %= BIT_ARRAY_LENGTH; bitStore.set((int) hashcode, true); } } /** * 進(jìn)行數(shù)據(jù)的查詢,判斷原數(shù)據(jù)中是否存在目標(biāo)查詢數(shù)據(jù) */ public Map queryDatasByBF() { boolean isExist; long hashcode; int pos1; int pos2; int pos3; // 查詢詞的所屬情況圖 Map word2exist = new HashMap (); hashcode = 0; isExist = false; bitStoreData(); for (String word : queryDatas) { isExist = false; hashcode = BKDRHash(word); pos1 = (int) (hashcode % BIT_ARRAY_LENGTH); hashcode = SDBMHash(word); pos2 = (int) (hashcode % BIT_ARRAY_LENGTH); hashcode = DJBHash(word); pos3 = (int) (hashcode % BIT_ARRAY_LENGTH); // 只有在3個哈希位置都存在才算真的存在 if (bitStore.get(pos1) && bitStore.get(pos2) && bitStore.get(pos3)) { isExist = true; } // 將結(jié)果存入map word2exist.put(word, isExist); } return word2exist; } /** * 進(jìn)行數(shù)據(jù)的查詢采用普通的過濾器方式就是,逐個查詢 */ public Map queryDatasByNF() { boolean isExist = false; // 查詢詞的所屬情況圖 Map word2exist = new HashMap (); // 遍歷的方式去查找 for (String qWord : queryDatas) { isExist = false; for (String word : totalDatas) { if (qWord.equals(word)) { isExist = true; break; } } word2exist.put(qWord, isExist); } return word2exist; } /** * BKDR字符哈希算法 * * @param str * @return */ private long BKDRHash(String str) { int seed = 31; /* 31 131 1313 13131 131313 etc.. */ long hash = 0; int i = 0; for (i = 0; i < str.length(); i++) { hash = (hash * seed) + (str.charAt(i)); } hash = Math.abs(hash); return hash; } /** * SDB字符哈希算法 * * @param str * @return */ private long SDBMHash(String str) { long hash = 0; int i = 0; for (i = 0; i < str.length(); i++) { hash = (str.charAt(i)) + (hash << 6) + (hash << 16) - hash; } hash = Math.abs(hash); return hash; } /** * DJB字符哈希算法 * * @param str * @return */ private long DJBHash(String str) { long hash = 5381; int i = 0; for (i = 0; i < str.length(); i++) { hash = ((hash << 5) + hash) + (str.charAt(i)); } hash = Math.abs(hash); return hash; } }
package BloomFilter; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Map; /** * BloomFileter布隆過濾器測試類 * * @author lyq * */ public class Client { public static void main(String[] args) { String filePath = "C:\\Users\\lyq\\Desktop\\icon\\input.txt"; String testFilePath = "C:\\Users\\lyq\\Desktop\\icon\\testInput.txt"; // 總的查詢詞數(shù) int totalCount; // 正確的結(jié)果數(shù) int rightCount; long startTime = 0; long endTime = 0; // 布隆過濾器查詢結(jié)果 MapbfMap; // 普通過濾器查詢結(jié)果 Map nfMap; //查詢總數(shù)據(jù) ArrayList queryDatas; BloomFilterTool tool = new BloomFilterTool(filePath, testFilePath); // 采用布隆過濾器的方式進(jìn)行詞的查詢 startTime = System.currentTimeMillis(); bfMap = tool.queryDatasByBF(); endTime = System.currentTimeMillis(); System.out.println("BloomFilter算法耗時" + (endTime - startTime) + "ms"); // 采用普通過濾器的方式進(jìn)行詞的查詢 startTime = System.currentTimeMillis(); nfMap = tool.queryDatasByNF(); endTime = System.currentTimeMillis(); System.out.println("普通遍歷查詢操作耗時" + (endTime - startTime) + "ms"); boolean isExist; boolean isExist2; rightCount = 0; queryDatas = tool.getQueryDatas(); totalCount = queryDatas.size(); for (String qWord: queryDatas) { // 以遍歷的查詢的結(jié)果作為標(biāo)準(zhǔn)結(jié)果 isExist = nfMap.get(qWord); isExist2 = bfMap.get(qWord); if (isExist == isExist2) { rightCount++; }else{ System.out.println("預(yù)判錯誤的詞語:" + qWord); } } System.out.println(MessageFormat.format( "Bloom Filter的正確個數(shù)為{0},總查詢數(shù)為{1}個,正確率{2}", rightCount, totalCount, 1.0 * rightCount / totalCount)); } }
BloomFilter算法耗時2ms 普通遍歷查詢操作耗時0ms Bloom Filter的正確個數(shù)為11,總查詢數(shù)為11個,【本文來自鴻網(wǎng)互聯(lián) (http://www.68idc.cn)】正確率1
BloomFilter算法耗時16ms 普通遍歷查詢操作耗時47ms Bloom Filter的正確個數(shù)為2,743,總查詢數(shù)為2,743個,正確率1
算法在實(shí)現(xiàn)的過程中遇到了一些小問題,第一就是在使用哈希函數(shù)的時候,因為我是隨機(jī)的選了3個字符哈希函數(shù),后來發(fā)現(xiàn)老是會越界,一越界數(shù)值就會變?yōu)樨?fù)的再通過BitSet就會報錯,原本在C語言中可以用unsigned int來解決,java中沒有這個概念,于是就直接取hash絕對值了。Bloom Filter算法的一個特點(diǎn)是數(shù)據(jù)可能會出現(xiàn)誤判,但是絕對不會漏判,誤判就是把不是存在集合中的元素判定成有,理由是哈希沖突可能造成此結(jié)果,而漏判指的是存在的元素判定成了不存在集合中,這個是絕對不可能的,因為如果你存在,你所代表的位置就一定會有被哈希映射到,一旦映射到了,在你再去查找就不會漏掉。算法的應(yīng)用范圍其實(shí)挺多的,典型的比如垃圾郵箱地址的過濾。
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com