由題引入:html
【騰訊】:2.給40億個不重複的無符號整數,沒排過序。給一個無符號整數,如何快速判斷一個數是否在這40億個數中。算法
若是將40億個數按整型放入內存,顯然不科學,就算內存足夠,這樣作也是浪費空間。數組
解決思路:用一個比特位表示一個數,存在的話該位上就置爲1,不在的話置爲0;這樣40億個數須要40億個比特位,換算一下也就是500M,相對於16G來講,大大節省了空間。安全
注意:位圖只適合判斷,查找數據是否存在,且只能對整數進行處理。數據結構
代碼以下:函數
1 class BitMap 2 { 3 public: 4 BitMap(size_t range) 5 :_size(0) 6 { 7 _a.resize((range >> 5) + 1); //多少個整型,一個整型表示32個數,加一是至少一個整型 8 } 9 void Set(size_t value) //存在時置1 10 { 11 size_t index = value >> 5; //計算在哪一個數據上 12 size_t num = value % 32; //計算在第幾個位上 13 (_a[index]) |= (1 << num); //將該位置1 14 _size++; 15 } 16 void Rset(size_t value) //不存在時或刪除一個數時置0 17 { 18 size_t index = value >> 5; //計算在哪一個數據上 19 size_t num = value % 32; //計算在第幾個位 20 (_a[index]) &= (~(1 << num)); //將該位置1 21 _size--; 22 } 23 bool JudgeBit(size_t value) //判斷該位的數值 24 { 25 size_t index = value >> 5; 26 size_t num = value % 32; 27 bool flag = (_a[index]) & (1 << num);//flag爲1即存在 28 return flag; 29 } 30 private: 31 vector<int> _a; //保存數據的數組 32 size_t _size; //數組中一共存在的數的總數 33 };
1 void BitMapTest() 2 { 3 BitMap bit(4000000000); 4 bit.Set(12000); 5 bit.Set(1200); 6 bit.Set(1205590); 7 bit.Set(12001); 8 bit.Set(12002); 9 bit.Set(12003); 10 bit.Set(12005); 11 cout << bit.JudgeBit(12090) << endl; 12 cout << bit.JudgeBit(120920) << endl; 13 cout << bit.JudgeBit(1205590) << endl; 14 } 15 16 int main() 17 { 18 BitMapTest(); 19 getchar(); 20 return 0;
優勢:(1)相對來講節省了很多空間。當須要處理的數量級較大時,這個優勢顯露無疑。大數據
(2)查找、刪除效率高。位圖只是在建立的時候開闢空間消耗時間,可是當位圖建立完成後查找、刪除只需一步操做。url
很明顯,用位圖只能用來處理整型,若是遇到字符型或者其餘類型的文件就無能爲力了,因此布隆過濾器就上場了。spa
布隆過濾器是由布隆在1970年提出的。它其實是由一個很長的二進制向量(運用位圖思想)和一系列隨機映射函數(哈希函數)組成,布隆過濾器能夠用於檢索一個元素是否在一個集合中。詳細來講就是將字符類型的數據經過哈希字符串函數轉換成整數,而後用位圖思想將一個數據用一個比特位表示起來。可是這樣作有一個缺點,就是判斷不許確,由於沒有一種算法能夠將字符串準確轉換爲惟一表示這個字符數據的整數。另外Hash面臨的問題就是衝突。假設 Hash 函數是良好的,若是咱們的位陣列長度爲 m 個點,那麼若是咱們想將衝突率下降到例如 1%, 這個散列表就只能容納 m/100 個元素。顯然這就不叫空間有效了(Space-efficient)。這裏提出一種改進方法:運用多種不一樣的哈希函數對同一字符數據進行轉化,將它們的轉換結果都存在表示在比特位中,那麼判斷的時候當這幾種哈希轉換結果表示的位都爲1時,字符數據存在,只要有一種方法轉化的位爲0,即不存在。簡單來講,就是多個 Hash中,若是它們有一個說元素不在集合中,那確定就不在。若是它們都說在,雖然也有必定可能性它們在說謊,不過直覺上判斷這種事情的機率是比較低的。debug
相比於其它的數據結構,布隆過濾器在空間和時間方面都有巨大的優點。布隆過濾器存儲空間和插入/查詢時間都是常數;另外, Hash 函數相互之間沒有關係,方便由硬件並行實現;布隆過濾器不須要存儲元素自己,在某些對保密要求很是嚴格的場合有優點;布隆過濾器能夠表示全集,其它任何數據結構都不能;k 和 m 相同,使用同一組 Hash 函數的兩個布隆過濾器的交併差運算可使用位操做進行。
布隆過濾器的缺點和優勢同樣明顯。誤算率是其中之一。隨着存入的元素數量增長,誤算率隨之增長。可是若是元素數量太少,則使用散列表足矣。另外,通常狀況下不能從布隆過濾器中刪除元素。 咱們很容易想到把位列陣變成整數數組,每插入一個元素相應的計數器加1, 這樣刪除元素時將計數器減掉就能夠了。然而要保證安全的刪除元素並不是如此簡單。首先咱們必須保證刪除的元素的確在布隆過濾器裏面。 這一點單憑這個過濾器是沒法保證的。另外計數器迴繞也會形成問題。
示例代碼以下:
1 //各種哈希函數 2 size_t BKDRHash(const char *str) 3 { 4 register size_t hash = 0; 5 while (size_t ch = (size_t)*str++) 6 { 7 hash = hash * 131 + ch; 8 } 9 return hash; 10 } 11 12 size_t SDBMHash(const char* str) 13 { 14 register size_t hash = 0; 15 while (size_t ch = (size_t)*str++) 16 { 17 hash = 65599 * hash + ch; 18 } 19 return hash; 20 } 21 size_t RSHash(const char * str) 22 { 23 size_t hash = 0; 24 size_t magic = 63689; 25 while (size_t ch = (size_t)*str++) 26 { 27 hash = hash * magic + ch; 28 magic *= 378551; 29 } 30 return hash; 31 } 32 size_t APHash(const char*str) 33 { 34 register size_t hash = 0; 35 size_t ch; 36 for (long i = 0; ch = (size_t)*str++; i++) 37 { 38 if ((i & 1) == 0) 39 { 40 hash ^= ((hash << 7) ^ ch ^ (hash >> 3)); 41 } 42 else 43 { 44 hash ^= (~((hash << 11) ^ ch ^ (hash >> 5))); 45 } 46 } 47 return hash; 48 } 49 size_t JSHash(const char* str) 50 { 51 if (!*str) 52 { 53 return 0; 54 } 55 size_t hash = 1315423911; 56 while (size_t ch = (size_t)*str++) 57 { 58 hash ^= ((hash << 5) + ch + (hash >> 2)); 59 } 60 return hash; 61 } 62 63 //哈希函數對應的仿函數 64 template<class K> 65 struct __HashFunc1 66 { 67 size_t operator()(const K& key) 68 { 69 return BKDRHash(key.c_str()); 70 } 71 }; 72 template<class K> 73 struct __HashFunc2 74 { 75 size_t operator()(const K& key) 76 { 77 return SDBMHash(key.c_str()); 78 } 79 }; 80 template<class K> 81 struct __HashFunc3 82 { 83 size_t operator()(const K& key) 84 { 85 return RSHash(key.c_str()); 86 } 87 }; 88 template<class K> 89 struct __HashFunc4 90 { 91 size_t operator()(const K& key) 92 { 93 return APHash(key.c_str()); 94 } 95 }; 96 template<class K> 97 struct __HashFunc5 98 { 99 size_t operator()(const K& key) 100 { 101 return JSHash(key.c_str()); 102 } 103 }; 104 105 template < class K = string, 106 class HashFunc1 = __HashFunc1<K>, 107 class HashFunc2 = __HashFunc2<K>, 108 class HashFunc3 = __HashFunc3<K>, 109 class HashFunc4 = __HashFunc4<K>, 110 class HashFunc5 = __HashFunc5<K>> 111 class BloomFilter 112 { 113 public: 114 BloomFilter(size_t range) 115 :_range(range) 116 { 117 _bitmap._a.resize((range >> 5) + 1); //初始空間 118 } 119 void _Set(const K& key ) 120 { 121 _bitmap.Set(HashFunc1()(key) % _range); 122 _bitmap.Set(HashFunc2()(key) % _range); 123 _bitmap.Set(HashFunc3()(key) % _range); 124 _bitmap.Set(HashFunc4()(key) % _range); 125 _bitmap.Set(HashFunc5()(key) % _range); 126 } 127 bool _JudgeBit(const K& key) 128 { 129 if (!_bitmap.JudgeBit(HashFunc1()(key) % _range))//只要有一個匹配不上就不存在 130 return false; 131 if (!_bitmap.JudgeBit(HashFunc2()(key) % _range)) 132 return false; 133 if (!_bitmap.JudgeBit(HashFunc3()(key) % _range)) 134 return false; 135 if (!_bitmap.JudgeBit(HashFunc4()(key) % _range)) 136 return false; 137 if (!_bitmap.JudgeBit(HashFunc5()(key) % _range)) 138 return false; 139 return true; 140 } 141 private: 142 BitMap _bitmap; //用位圖表示轉換後的數 143 size_t _range; // 144 };
1.給一個超過100G大小的log file, log中存着IP地址, 設計算法找到出現次數最多的IP地址?!
2.與上題條件相同,如何找到top K的IP?如何直接用Linux系統命令實現?!
3.給定100億個整數,設計算法找到只出現一次的整數!
4.給兩個文件,分別有100億個整數,咱們只有1G內存,如何找到兩個文件交集!
5.1個文件有100億個int,1G內存,設計算法找到出現次數不超過2次的全部整數!
6.給兩個文件,分別有100億個url,咱們只有1G內存,如何找到兩個文件交集?分別給出精確 算法和近似算法!
7.如何擴展BloomFilter使得它支持刪除元素的操做?如何擴展BloomFilter使得它支持計數操做?!
8.給上千個文件,每一個文件大小爲1K—100M。給n個詞,設計算法對每一個詞找到全部包含它文 件,你只有100K內存!
9.有一個詞典,包含N個英文單詞,如今任意給一個字符串,設計算法找出包含這個字符串的所 有英文單詞!
第1題、給一個超過100G大小的log file, log中存着IP地址, 設計算法找到出現次數最多的IP地址
這是一個大小爲100G的一個日誌文件,主要問題就是通常的計算機內存確定放不下;第一個想到的辦法就是切分,把100G的文件切成100份,而後把這100個文件看成是大小爲100的哈希表,而每份只有1G的大小,就能夠依次讀入內存進行處理。題目要求是:找到出現次數最多的IP地址,那麼文件中確定存在大量的相同IP地址,思路是讓相同的IP存入同一文件,這時又要用到哈希字符串函數,就是上面布隆過濾器用到的轉換函數,由相同的IP轉換獲得的key值必定相同,而後根據index = key%100決定存在於哪個文件中,而相同的IP也就進入了同一個文件。
而後對單個文件進行處理,找出這個文件中出現次數最多的IP,以IP爲key值,value記錄出現的次數,用key_value結構的搜索樹就能夠很快找出來,而後用MAX記錄下來,讀入下一個文件,而後比較MAX值,遇到更大的就更新,最後獲得的MAX就是這個100G文件中出現次數最多的IP地址。
這個題目中的重點思想就是哈希切分。
第2題、一個超過100G大小的log file, 存着IP地址,找到top K的IP。如何直接用Linux系統命令實現?
這一題條件同上一題,不一樣的是由求次數最多的一個改成求次數最多的前K個。思路同上題,哈希切分而後用堆排序,仍是以IP爲key值,而後統計各個文件中每一個IP出現的次數(方法同第一題,也就是說每一個文件建一顆搜索樹), 而後取其中的K個(key_value結構)結點以次數建一個最小堆;而後將其他的節點依次與堆頂節點比較,若是大於堆頂節點,與其一換,交換以後對堆進行一次向下調整,保證堆頂元素還是堆中最小,直到全部IP都比較完。而後堆中的就是top K個IP了。
這題是個典型的top K問題,重點是建小堆,而後交換堆頂元素。
第3題、給定100億個整數,找到只出現一次的整數
與上面一樣的一個問題是100億整數這樣一個龐大的數字,大約是35G的大小。可是整數能表示的最大範圍也就是2的32次方 那麼大約就是16G的大小,那麼剩下的就都是重複的數,這道題沒有規定死內存大小,可是16G仍是比較大,浪費內存資源,如何繼續縮小內存,仍是利用位圖思想。與前例騰訊筆試題不一樣的是,這裏須要區分更多的狀態,咱們須要表示的狀態有:00不存在, 01出現一次,10出現屢次(>=2次),11不表示。也就是說咱們須要用兩個比特位來表示一個數的狀態,而後遍歷一遍位圖找到狀態爲01的數,就是隻出現一次的整數。
這個題重點是兩個比特位的位圖思想。
第4題、兩個文件,分別有100億個整數,咱們只有1G內存,找到兩個文件的交集
此題初始思路同上,創建位圖,不在贅述,這裏主要講求交集。能夠對其中一個文件創建位圖,而後從另外一個文件中依次取數據,判斷是否在位圖中。數據判斷完存在的即爲交集。另外一種思路,若是這裏還有1G的內存的話,能夠給兩個文件分別鍵位圖,而後比較對應的數據位。
第二種方法是哈希切分,將兩個文件都切分爲1000小份,每一個文件的大小就幾十兆的樣子,分別對兩個對文件裏的整數進行哈希分配,即將全部整數模除1000,使相同的數進入相同的文件,而後分別拿A哈希切分好的第一個文件和B哈希切分好的第一個文件對比,找出交集存到一個新文件中,依次類推,直到2000個文件互相比較完。
這個題重點是位圖思想和哈希切分。
第5題、1個文件有100億個int,1G內存,找到出現次數不超過2次的全部整數
這個題思路同第三題,用兩個比特位表示的位圖,咱們須要表示的狀態有:00不存在, 01出現1次,10出現2次,11出現屢次(>2次)。
這個題重點也是兩個比特位的位圖思想。
第6題、兩個文件,分別有100億個url,咱們只有1G內存,找到兩個文件交集,分別給出精確算法和近似算法。
與第四題相似只是這裏存的是URL,因此要用布隆過濾器。近似算法是,將一個文件內容存到布隆過濾器中,方法如上面介紹的布隆過濾器中的同樣,而後從另外一個文件中一個個的取URL判斷是否在布隆中存在的就是交集。爲何布隆過濾器是近似算法,是由於它的不存在是肯定的,存在是不肯定的,即一個字符串對應5個位, 若是有一個位爲0,則這字符串確定不存在,若是一個字符串對應的5個位都爲1,可是這個字符串卻不 必定存在,由於可能這5個位都是被其它字符串的對應位置爲1的,這就是其中的哈希衝突問題。
精確算法同第四題的方法二,哈希切分。
第7題、擴展BloomFilter使得它支持刪除元素的操做或支持計數操做
由於布隆過濾器的一個Key對應多個位,因此若是要刪除的話,就會有些麻煩,不能單純的將對應位所有置爲0,由於可能還有其它key對應這些位,因此,須要對每個位進行引用計數,以實現刪除的操做。由於須要每個對應位都須要一個計數,因此每一位至少須要一個int,那麼咱們就不得不放棄位圖了,也就是放棄了最小的空間消耗,咱們須要直接以一個就像數組同樣的實現,只不過數組的內容存放的是引用計數。
代碼示例以下:
1 //支持引用計數,能夠刪除元素的布隆 2 template <class K = string> 3 class NumBloom 4 { 5 size_t HashFunc1(const K& key) 6 { 7 const char* str = key.c_str(); 8 unsigned int seed = 131; 9 unsigned int hash = 0; 10 while (*str) 11 { 12 hash = hash*seed + (*str++); 13 } 14 return(hash & 0x7FFFFFFF); 15 }; 16 size_t HashFunc2(const K& key) 17 { 18 const char* str = key.c_str(); 19 register size_t hash = 0; 20 while (size_t ch = (size_t)*str++) 21 { 22 hash = 65599 * hash + ch; 23 //hash = (size_t)ch + (hash << 6) + (hash << 16) - hash; 24 } 25 return hash; 26 } 27 size_t HashFunc3(const K& key) 28 { 29 const char* str = key.c_str(); 30 register size_t hash = 0; 31 size_t magic = 63689; 32 while (size_t ch = (size_t)*str++) 33 { 34 hash = hash * magic + ch; 35 magic *= 378551; 36 } 37 return hash; 38 } 39 size_t HashFunc4(const K& key) 40 { 41 const char* str = key.c_str(); 42 register size_t hash = 0; 43 size_t ch; 44 for (long i = 0; ch = (size_t)*str++; i++) 45 { 46 if ((i & 1) == 0) 47 { 48 hash ^= ((hash << 7) ^ ch ^ (hash >> 3)); 49 } 50 else 51 { 52 hash ^= (~((hash << 11) ^ ch ^ (hash >> 5))); 53 } 54 } 55 return hash; 56 } 57 size_t HashFunc5(const K& key) 58 { 59 const char* str = key.c_str(); 60 if (!*str) 61 return 0; 62 register size_t hash = 1315423911; 63 while (size_t ch = (size_t)*str++) 64 { 65 hash ^= ((hash << 5) + ch + (hash >> 2)); 66 } 67 return hash; 68 } 69 public: 70 NumBloom(const size_t range) 71 { 72 _bloom.resize(range); //初始空間 73 for (size_t i = 0; i < _bloom.size(); i++) 74 { 75 _bloom[i] = 0; 76 } 77 } 78 void _Set(const K& key) 79 { 80 _bloom[HashFunc1(key) % _bloom.size()]; 81 _bloom[HashFunc1(key) % _bloom.size()]; 82 _bloom[HashFunc1(key) % _bloom.size()]; 83 _bloom[HashFunc1(key) % _bloom.size()]; 84 _bloom[HashFunc1(key) % _bloom.size()]; 85 _bloom[HashFunc1(key) % _bloom.size()]; 86 } 87 void _Reset(const K& key) 88 { 89 if (_bloom[HashFunc1(key) % _bloom.size()] == 0) 90 return false; 91 if (_bloom[HashFunc2(key) % _bloom.size()] == 0) 92 return false; 93 if (_bloom[HashFunc3(key) % _bloom.size()] == 0) 94 return false; 95 if (_bloom[HashFunc4(key) % _bloom.size()] == 0) 96 return false; 97 if (_bloom[HashFunc5(key) % _bloom.size()] == 0) 98 return false; 99 _bloom[HashFunc1(key) % _bloom.size()]--; 100 _bloom[HashFunc2(key) % _bloom.size()]--; 101 _bloom[HashFunc3(key) % _bloom.size()]--; 102 _bloom[HashFunc4(key) % _bloom.size()]--; 103 _bloom[HashFunc5(key) % _bloom.size()]--; 104 } 105 bool _JudgeBit(const K& key) 106 { 107 if (_bloom[HashFunc1(key) % _bloom.size()] == 0)//只要有一個匹配不上就不存在 108 return false; 109 if (_bloom[HashFunc2(key) % _bloom.size()] == 0)//只要有一個匹配不上就不存在 110 return false; 111 if (_bloom[HashFunc3(key) % _bloom.size()] == 0)//只要有一個匹配不上就不存在 112 return false; 113 if (_bloom[HashFunc4(key) % _bloom.size()] == 0)//只要有一個匹配不上就不存在 114 return false; 115 if (_bloom[HashFunc5(key) % _bloom.size()] == 0)//只要有一個匹配不上就不存在 116 return false; 117 return true; 118 } 119 120 private: 121 vector<size_t> _bloom; 122 };
第8題、給上千個文件,每一個文件大小爲1K—100M。給n個詞,設計算法對每一個詞找到全部包含它的文件,你只有100K內存!
牛客網上的解析:
0: 用一個文件info 準備用來保存n個詞和包含其的文件信息。
1 : 首先把n個詞分紅x份。對每一份用生成一個布隆過濾器(由於對n個詞只生成一個布隆過濾器,內存可能不夠用)。把生成的全部布隆過濾器存入外存的一個文件Filter中。2:將內存分爲兩塊緩衝區,一塊用於每次讀入一個布隆過濾器,一個用於讀文件(讀文件這個緩衝區使用至關於有界生產者消費者問題模型來實現同步),大文件能夠分爲更小的文件,但須要存儲大文件的標示信息(如這個小文件是哪一個大文件的)。
3:對讀入的每個單詞用內存中的布隆過濾器來判斷是否包含這個值,若是不包含,從Filter文件中讀取下一個布隆過濾器到內存,直到包含或遍歷完全部布隆過濾器。若是包含,更新info 文件。直處處理完全部數據。刪除Filter文件。
備註:
1:關於布隆過濾器:其實就是一張用來存儲字符串hash值的BitMap.
2:可能還有一些細節問題,如重複的字符串致使的重複計算等要考慮一下。
第9題、有一個詞典,包含N個英文單詞,如今任意給一個字符串,設計算法找出包含這個字符串的所 有英文單詞!
思路:用kmp算法或者字典樹,KMP算法可見個人另外一篇文章:字符串模式匹配問題