百度面試題:給定a、b兩個文件,各存放50億個url,每個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?
Bloom Filter是由Bloom在1970年提出的一種多哈希函數映射的快速查找算法。通常應用在一些需要快速判斷某個元素是否屬於集合,但是並不嚴格要求100%正確的場合。
一. 實例
爲了說明Bloom Filter存在的重要意義,舉一個實例:
(實例一),假設要你寫一個網絡蜘蛛(web crawler)。由於網絡間的鏈接錯綜複雜,蜘蛛在網絡間爬行很可能會形成“環”。爲了避免形成“環”,就需要知道蜘蛛已經訪問過那些URL。給一個URL,怎樣知道蜘蛛是否已經訪問過呢?稍微想想,
(實例二)給定a、b兩個文件,各存放50億個url,每個url各佔64字節,內存限制是4G,讓你找出a、b文件共同的url?
就會有如下幾種方案:
1. 將訪問過的URL保存到數據庫。
2. 用HashSet將訪問過的URL保存起來。那隻需接近O(1)的代價就可以查到一個URL是否被訪問過了。
3. URL經過MD5或SHA-1等單向哈希後再保存到HashSet或數據庫。
4. Bit-Map方法。建立一個BitSet,將每個URL經過一個哈希函數映射到某一位。
方法1~3都是將訪問過的URL完整保存,方法4則只標記URL的一個映射位。
以上方法在數據量較小的情況下都能完美解決問題,但是當數據量變得非常龐大時問題就來了。
方法1的缺點:數據量變得非常龐大後關係型數據庫查詢的效率會變得很低。而且每來一個URL就啓動一次數據庫查詢是不是太小題大做了?
方法2的缺點:太消耗內存。隨着URL的增多,佔用的內存會越來越多。就算只有1億個URL,每個URL只算50個字符,就需要5GB內存。
方法3:由於字符串經過MD5處理後的信息摘要長度只有128Bit,SHA-1處理後也只有160Bit,因此方法3比方法2節省了好幾倍的內存。
方法4消耗內存是相對較少的,但缺點是單一哈希函數發生衝突的概率太高。還記得數據結構課上學過的Hash表衝突的各種解決方法麼?若要降低衝突發生的概率到1%,就要將BitSet的長度設置爲URL個數的100倍。
實質上上面的算法都忽略了一個重要的隱含條件:允許小概率的出錯,不一定要100%準確!也就是說少量url實際上沒有沒網絡蜘蛛訪問,而將它們錯判爲已訪問的代價是很小的——大不了少抓幾個網頁唄。
例如有 一組字符 arr:”哈哈“,”呵呵“........
字符串:“哈哈”
哈希算法1處理後:8
哈希算法2處理後:1
哈希算法1處理後:3
插入BitArray後
再處理字符串:“呵呵”
哈希算法1處理後:2
哈希算法2處理後:1
哈希算法1處理後:9
繼續插入BitArray後,如果繼續遊字符串,繼續以這種方式插入
判斷”在這些字符串是否包含”嘻嘻“
哈希算法1處理後:0
哈希算法2處理後:1
哈希算法1處理後:7
只要判斷 下標分別爲 0,1,7位置的值是否都爲1,如下圖 因爲位置0跟位置7的值不爲1
所以”嘻嘻“不包含在arr中,反之如果都爲1怎包含
java代碼實現如下
import java.util.ArrayList;
import java.util.BitSet;
import java.util.List;
/**
* BloomFilter算法
*
* @author JYC506
*
*/
public class BloomFilter {
/*哈希函數*/
private List<IHashFunction> hashFuctionList;
/*構造方法*/
public BloomFilter() {
this.hashFuctionList = new ArrayList<IHashFunction>();
}
/*添加哈希函數類*/
public void addHashFunction(IHashFunction hashFunction) {
this.hashFuctionList.add(hashFunction);
}
/*刪除hash函數*/
public void removeHashFunction(IHashFunction hashFunction) {
this.hashFuctionList.remove(hashFunction);
}
/*判斷是否被包含*/
public boolean contain(BitSet bitSet, String str) {
for (IHashFunction hash : hashFuctionList) {
int hashCode = hash.toHashCode(str);
if(hashCode<0){
hashCode=-hashCode;
}
if (bitSet.get(hashCode) == false) {
return false;
}
}
return true;
}
/*添加到bitSet*/
public void toBitSet(BitSet bitSet, String str) {
for (IHashFunction hash : hashFuctionList) {
int hashCode = hash.toHashCode(str);
if(hashCode<0){
hashCode=-hashCode;
}
bitSet.set(hashCode, true);
}
}
public static void main(String[] args) {
BloomFilter bloomFilter=new BloomFilter();
/*添加3個哈希函數*/
bloomFilter.addHashFunction(new JavaHash());
bloomFilter.addHashFunction(new RSHash());
bloomFilter.addHashFunction(new SDBMHash());
/*長度爲2的24次方*/
BitSet bitSet=new BitSet(1<<25);
/*判斷test1很test2重複的字符串*/
String[] test1=new String[]{"哈哈","我","大家","逗比","有錢人性","小米","Iphone","helloWorld"};
for (String str1 : test1) {
bloomFilter.toBitSet(bitSet, str1);
}
String[] test2=new String[]{"哈哈","我的","大家","逗比","有錢的人性","小米","Iphone6s","helloWorld"};
for (String str2 : test2) {
if(bloomFilter.contain(bitSet, str2)){
System.out.println("'"+str2+"'是重複的");
}
}
}
}
/*哈希函數接口*/
interface IHashFunction {
int toHashCode(String str);
}
class JavaHash implements IHashFunction {
@Override
public int toHashCode(String str) {
return str.hashCode();
}
}
class RSHash implements IHashFunction {
@Override
public int toHashCode(String str) {
int b = 378551;
int a = 63689;
int hash = 0;
for (int i = 0; i < str.length(); i++) {
hash = hash * a + str.charAt(i);
a = a * b;
}
return hash;
}
}
class SDBMHash implements IHashFunction {
@Override
public int toHashCode(String str) {
int hash = 0;
for (int i = 0; i < str.length(); i++)
hash = str.charAt(i) + (hash << 6) + (hash << 16) - hash;
return hash;
}
}
運行結果