第二部分:Hash表演算法的詳細解析
什麼是Hash
Hash,一般翻譯做“散列”,也有直接音譯為“雜湊”的,就是把任意長度的輸入(又叫做預映射, pre-image),通過散列演算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不 同的輸入可能會散列成相同的輸出,而不可能從散列值來唯一的確定輸入值。簡單的說就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函數。
HASH主要用於資訊安全領域中密碼編譯演算法,它把一些不同長度的資訊轉化成雜亂的128位的編碼,這些編碼值叫做HASH值. 也可以說,hash就是找到一種資料內容和資料存放地址之間的映射關係。
數組的特點是:定址容易,插入和刪除困難;而鏈表的特點是:定址困難,插入和刪除容易。那麼我們能不能綜合兩者的特性,做出一種定址容易,插入刪除 也容易的資料結構。答案是肯定的,這就是我們要提起的雜湊表,雜湊表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鏈法,我們可以理解為 “鏈表的數組”,如圖:
左邊很明顯是個數組,數組的每個成員包括一個指標,指向一個鏈表的頭,當然這個鏈表可能為空白,也可能元素很多。我們根據元素的一些特徵把元素分配到不同的鏈表中去,也是根據這些特徵,找到正確的鏈表,再從鏈表中找出這個元素。
元素特徵轉變為數組下標的方法就是散列法。散列法當然不止一種,下面列出三種比較常用的:
1,除法散列法
最直觀的一種,上圖使用的就是這種散列法,公式:
index = value % 16
學過彙編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法散列法”。
2,平方散列法
求index是非常頻繁的操作,而乘法的運算要比除法來得省時(對現在的CPU來說,估計我們感覺不出來),所以我們考慮把除法換成乘法和一個位移操作。公式:
index = (value * value) >> 28 (右移,除以2^28。記法:左移變大,是乘。右移變小,是除。)
如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都 是0——非常失敗。也許你 還有個問題,value如果很大,value * value不會溢出嗎。答案是會的,但我們這個乘法不關心溢出,因為我們根本不是為了擷取相乘結果,而是為了擷取index。
3,斐波那契(Fibonacci)散列法
平方散列法的缺點是顯而易見的,所以我們能不能找出一個理想的乘數,而不是拿value本身當作乘數呢。答案是肯定的。
1,對於16位整數而言,這個乘數是40503
2,對於32位整數而言,這個乘數是2654435769
3,對於64位整數而言,這個乘數是11400714819323198485
這幾個“理想乘數”是如何得出來的呢。這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典運算式無疑就是著名的斐波那契數列,即如此形 式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契數列的值和太陽系八大行星的軌道半徑的比例出奇吻合。
對我們常見的32位整數而言,公式:
index = (value * 2654435769) >> 28
如果用這種斐波那契散列法的話,那上面的圖就變成這樣了:
很明顯,用斐波那契散列法調整之後要比原來的取摸散列法好很多。
適用範圍
快速尋找,刪除的基本資料結構,通常需要總資料量可以放入記憶體。
基本原理及要點
hash函數選擇,針對字串,整數,排列,具體相應的hash方法。
碰撞處理,一種是open hashing,也稱為拉鏈法;另一種就是closed hashing,也稱開地址法,opened addressing。
擴充
d-left hashing中的d是多個的意思,我們先簡化這個問題,看一看2-left hashing。2-left hashing指的是將一個雜湊表分成長度相等的兩半,分別叫做T1和T2,給T1和T2分別配備一個雜湊函數,h1和h2。在儲存一個新的key時,同 時用兩個雜湊函數進行計算,得出兩個地址h1[key]和h2[key]。這時需要檢查T1中的h1[key]位置和T2中的h2[key]位置,哪一個 位置已經儲存的(有碰撞的)key比較多,然後將新key儲存在負載少的位置。如果兩邊一樣多,比如兩個位置都為空白或者都儲存了一個key,就把新key 儲存在左邊的T1子表中,2-left也由此而來。在尋找一個key時,必須進行兩次hash,同時尋找兩個位置。
問題執行個體(海量資料處理)
我們知道hash 表在海量資料處理中有著廣泛的應用,下面,請看另一道百度面試題:
題目:海量日誌資料,提取出某日訪問百度次數最多的那個IP。
方案:IP的數目還是有限的,最多2^32個,所以可以考慮使用hash將ip直接存入記憶體,然後進行統計。
第三部分、最快的Hash表演算法
接下來,咱們來具體分析一下一個最快的Hasb表演算法。
我們由一個簡單的問題逐步入手:有一個龐大的字串數組,然後給你一個單獨的字串,讓你從這個數組中尋找是否有這個字串並找到它,你會怎麼做。 有一個 方法最簡單,老老實實從頭查到尾,一個一個比較,直到找到為止,我想只要學過程式設計的人都能把這樣一個程式作出來,但要是有程式員把這樣的程式交給用 戶,我只能用無語來評價,或許它真的能工作,但…也只能如此了。
最合適的演算法自然是使用HashTable(雜湊表),先介紹介紹其中的基本知識,所謂Hash,一般是一個整數,通過某種演算法,可以把一個字元 串”壓縮” 成一個整數。當然,無論如何,一個32位整數是無法對應回一個字串的,但在程式中,兩個字串計算出的Hash值相等的可能非常小,下面看看在MPQ中 的Hash演算法:
函數一、以下的函數產生一個長度為0×500(合10進位數:1280)的cryptTable[0x500]
void prepareCryptTable() { unsigned long seed = 0x00100001, index1 = 0, index2 = 0, i; for( index1 = 0; index1 < 0x100; index1++ ) { for( index2 = index1, i = 0; i < 5; i++, index2 += 0x100 ) { unsigned long temp1, temp2; seed = (seed * 125 + 3) % 0x2AAAAB; temp1 = (seed & 0xFFFF) << 0x10; seed = (seed * 125 + 3) % 0x2AAAAB; temp2 = (seed & 0xFFFF); cryptTable[index2] = ( temp1 | temp2 ); } } }
函數二、以下Function ComputelpszFileName 字串的hash值,其中dwHashType 為hash的類型,在下面的函數三、GetHashTablePos函數中調用此函數二,其可以取的值為0、1、2;該函數返回lpszFileName 字串的hash值:
unsigned long <strong>HashString</strong>( char *lpszFileName, unsigned long dwHashType ) { unsigned char *key = (unsigned char *)lpszFileName; unsigned long seed1 = 0x7FED7FED; unsigned long seed2 = 0xEEEEEEEE; int ch; while( *key != 0 ) { ch = toupper(*key++); seed1 = cryptTable[(dwHashType << 8) + ch] ^ (seed1 + seed2); seed2 = ch + seed1 + seed2 + (seed2 << 5) + 3; } return seed1; }
Blizzard的這個演算法是非常高效的,被稱為”One-Way Hash”( A one-way hash is a an algorithm that is constructed in such a way that deriving the original string (set of strings, actually) is virtually impossible)。舉個例子,字串”unitneutralacritter.grp”通過這個演算法得到的結果是0xA26067F3。
是不是把第一個演算法改進一下,改成逐個比較字串的Hash值就可以了呢,答案是,遠遠不夠,要想得到最快的演算法,就不能進行逐個的比較,通常是構造一個雜湊表(Hash Table)來解決問題,雜湊表是一個大數組,這個數組的容量根據程式的要求來定義,例如1024,每一個Hash值通過模數運算 (mod) 對應到數組中的一個位置,這樣,只要比較這個字串的雜湊值對應的位置有沒有被佔用,就可以得到最後的結果了,想想這是什麼速度。是的,是最快的 O(1),現在仔細看看這個演算法吧:
typedef struct { int nHashA; int nHashB; char bExists; …… } SOMESTRUCTRUE;
一種可能的結構體定義。
函數三、下述函數為在Hash表中尋找是否存在目標字串,有則返回要尋找字串的Hash值,無則,return -1.
int <strong>GetHashTablePos</strong>( har *lpszString, SOMESTRUCTURE *lpTable ) //lpszString要在Hash表中尋找的字串,lpTable為儲存字串Hash值的Hash表。 { int nHash = HashString(lpszString);