筆者在讀研剛開始的時候,偶爾看面經,有這樣一個問題:只用2GB內存在20億個整數中找到出現次數最多的數,當時的我一臉懵逼,怎麼去思考,20億個數?What The Fuck! 但是,看完今天的文章,你或許就會覺得原來也不過如此啊!其核心就是哈希函數和哈希表的應用!
哈希函數哈希函數又稱為散列函數,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。假設輸出值域為S,哈希函數的性質如下:
典型的哈希函數都有無限的輸入值域
當哈希函數輸入一致時,輸出必相同
當哈希函數傳入不同的輸入值時,返回值可能一樣,也可能不一樣,由於輸入域遠大於值域
(重要)很多的不同輸入所得的輸出值會均勻的分布在S上(但不是絕對均勻)
最後一個性質對於一個優秀的哈希函數是非常重要的,並且這種均勻與數據的輸入規律無關。比如「aa1」、"aa2"經過hash後可能結果會相差很多,當一個哈希函數的輸出在S中是均勻的,那麼我們將輸出值對m取餘(%m),就會將不定長輸入映射到0~m-1空間中,並且在這個空間也保持均勻分布!哈希表就是這麼做的,一會再說!哈希函數還有以下特點:
免碰撞:即不會出現輸入 x≠y ,但是H(x)=H(y) 的情況,其實這個特點在理論上並不成立,比如目前比特幣使用的 SHA256 算法,會有2^256種輸出,如果我們進行2^256 + 1 次輸入,那麼必然會產生一次碰撞,事實上,通過 理論證明 ,通過2^130次輸入就會有99%的可能性發生一次碰撞,不過即使如此,即便是人類製造的所有計算機自宇宙誕生開始一直運算到今天,發生一次碰撞的機率也是極其微小的。
隱匿性:也就是說,對於一個給定的輸出結果 H(x) ,想要逆推出輸入 x ,在計算上是不可能的。如果想要得到 H(x) 的可能的原輸入,不存在比窮舉更好的方法。
常見的哈希函數有:SHA1、MD5、SHA2等
哈希表就是利用哈希函數,可以根據關鍵碼而直接進行訪問的數據結構,也就是將關鍵碼(Key value)通過哈希函數映射到表中的一個位置來進行訪問。由於是直接訪問,所以對於哈希表的元素理論上的增刪改查時間複雜度都是O(1)。
由於我們的輸入長度和範圍是任意的,但是經過哈希函數後的輸出值域是固定的,所以必然會產生衝突。如上圖的buckets152(紅色區域)就相當於發生衝突!處理衝突的方法有:
我們來說下拉鏈法,也如上圖所示,拉鏈法的思路很簡單,就是當發生哈希衝突後,會在當前地址區域建立一個鍊表,將衝突目標添加到鍊表中去。這種方式也不太好,當衝突發生過多時,鍊表的查找方式效率也就不是很高了!
因此對於JAVA中(C++標準中沒有hashmap,只有第三方的),hashmap的實現也是類似,但是有一點改進,也就是如果發生衝突,將衝突對象添加到鍊表,假設衝突個數達到了8次,那麼就會使用紅黑樹來代替鍊表,以加快查找速度。衝突個數少時,沒有必要使用紅黑樹。
c++的hash_map和map的用法很類似,但一定要區別,map和hash_map雖然都是key-value形式,但是map的底層是紅黑樹,而hash_map的底層是hash表!
如果在Linux下使用hash_map,一定要加上一個__gnu_cxx的命名空間聲明!
#include<iostream>
#include<hash_map>
using namespace __gnu_cxx;
using namespace std;
int main(int args, char** argv){
hash_map<int, string>* has = new hash_map<int, string>;
has->insert(make_pair(32, "zhang"));
has->insert({0, "teddy"});
auto res = has->find(0);
auto res2 = has->find(32);
cout << res->second << endl;
cout << res2->second << endl;
return 0;
}
首先我們確定value的範圍,如果一個數出現了20億次,那麼value就為20億次。因此我們使用32位的正整型,也就是4B的空間,同理key也是4B的空間,因此一條記錄(Entry)需要8B的空間,當記錄為20億個時,需要至少16GB的內存。
在極端最差的狀態,20億個數都不相同,那麼哈希表中可能會有20億條記錄,這樣的話顯然內存不足,因此一次性統計20個數風險很大。
解決方案:將包含有20億個數的大文件分成16個小文件,利用哈希函數,這樣的話,同一個重複的數肯定不會分到不同的文件中去,並且,如果哈希函數足夠好,那麼這16個文件中不同的數也不會大於2億(20 / 16)。然後我們在這16個文件中依次統計就可以了,最後進行匯總得到重複數最多的數。當然如果使用分布式系統,那麼可以利用哈希函數將這些數據分配到不同的電腦上去!
資源連結
如果想要看更多精彩內容,請關注我的個人公眾號 (算法工程師之路)希望大家多多支持哦~
公眾號簡介:分享算法工程師必備技能,談談那些有深度有意思的算法,主要範圍:C++數據結構與算法/深度學習(CV),立志成為Offer收割機!堅持分享算法題目和解題思路(Day By Day)