一、以下引用自什么是哈希算法
比如這里有一萬首歌,給你一首新的歌X,要求你確認這首歌是否在那一萬首歌之內。
無疑,將一萬首歌一個一個比對非常慢。但如果存在一種方式,能將一萬首歌的每首數據濃縮到一個數字(稱為哈希碼)中,于是得到一萬個數字,那么用同樣的算法計算新的歌X的編碼,看看歌X的編碼是否在之前那一萬個數字中,就能知道歌X是否在那一萬首歌中。
作為例子,如果要你組織那一萬首歌,一個簡單的哈希算法就是讓歌曲所占硬盤的字節數作為哈希碼。這樣的話,你可以讓一萬首歌“按照大小排序”,然后遇到一首新的歌,只要看看新的歌的字節數是否和已有的一萬首歌中的某一首的字節數相同,就知道新的歌是否在那一萬首歌之內了。
當然這個簡單的哈希算法很容易出現兩者同樣大小的歌曲,這就是發生了碰撞。而好的哈希算法發生碰撞的幾率非常小。
二、以下引用自哈希表(Hash Table)及散列法(Hashing)
數組的特點是:尋址容易,插入和刪除困難;而鏈表的特點是:尋址困難,插入和刪除容易。那么我們能不能綜合兩者的特性,做出一種尋址容易,插入刪除也容易的數據結構?答案是肯定的,這就是我們要提起的哈希表,哈希表有多種不同的實現方法,我接下來解釋的是最常用的一種方法——拉鏈法,我們可以理解為“鏈表的數組”,如圖:
左邊很明顯是個數組,數組的每個成員包括一個指針,指向一個鏈表的頭,當然這個鏈表可能為空,也可能元素很多。我們根據元素的一些特征把元素分配到不同的鏈表中去,也是根據這些特征,找到正確的鏈表,再從鏈表中找出這個元素。元素特征轉變為數組下標的方法就是散列法。散列法當然不止一種,我下面列出三種比較常用的。
- 除法散列法
最直觀的一種,上圖使用的就是這種散列法,公式:
index = value % 16
學過匯編的都知道,求模數其實是通過一個除法運算得到的,所以叫“除法散列法”。 - 平方散列法
求index是非常頻繁的操作,而乘法的運算要比除法來得省時(對現在的CPU來說,估計我們感覺不出來),所以我們考慮把除法換成乘法和一個位移操作。公式:
index = (value * value) >> 28
如果數值分配比較均勻的話這種方法能得到不錯的結果,但我上面畫的那個圖的各個元素的值算出來的index都是0——非常失敗。也許你還有個問題,value如果很大,value * value不會溢出嗎?答案是會的,但我們這個乘法不關心溢出,因為我們根本不是為了獲取相乘結果,而是為了獲取index。 - 斐波那契(Fibonacci)散列法
平方散列法的缺點是顯而易見的,所以我們能不能找出一個理想的乘數,而不是拿value本身當作乘數呢?答案是肯定的。 - 對于16位整數而言,這個乘數是40503
- 對于32位整數而言,這個乘數是2654435769
- 對于64位整數而言,這個乘數是11400714819323198485
這幾個“理想乘數”是如何得出來的呢?這跟一個法則有關,叫黃金分割法則,而描述黃金分割法則的最經典表達式無疑就是著名的斐波那契數列,如果你還有興趣,就到網上查找一下“斐波那契數列”等關鍵字,我數學水平有限,不知道怎么描述清楚為什么,另外斐波那契數列的值居然和太陽系八大行星的軌道半徑的比例出奇吻合,很神奇,對么?
對我們常見的32位整數而言,公式:
index = (value * 2654435769) >> 28
如果用這種斐波那契散列法的話,那我上面的圖就變成這樣了:
三、以下引自暴雪公司經典字符串hash公式
中國有句古話"再一再二不能再三再四",看來Blizzard也深得此話的精髓,如果說兩個不同的字符串經過一個哈希算法得到的入口點一致有可能,但用三個不同的哈希算法算出的入口點都一致,那幾乎可以肯定是不可能的事了,這個幾率是1:18889465931478580854784,大概是10的 22.3次方分之一,對一個游戲程序來說足夠安全了。
現在再回到數據結構上,Blizzard使用的哈希表沒有使用鏈表,而采用"順延"的方式來解決問題,看看這個算法:
int GetHashTablePos(char *lpszString, MPQHASHTABLE *lpTable, int nTableSize)
{
const int HASH_OFFSET = 0, HASH_A = 1, HASH_B = 2;
int nHash = HashString(lpszString, HASH_OFFSET);
int nHashA = HashString(lpszString, HASH_A);
int nHashB = HashString(lpszString, HASH_B);
int nHashStart = nHash % nTableSize, nHashPos = nHashStart;
while (lpTable[nHashPos].bExists)
{ //比較的是Table中存儲的另外兩個Hash函數的值,Table中不存儲字符串
if (lpTable[nHashPos].nHashA == nHashA && lpTable[nHashPos].nHashB == nHashB)
return nHashPos;
else //沖突處理
nHashPos = (nHashPos + 1) % nTableSize;
if (nHashPos == nHashStart)
break;
}
return -1; //Error value
}
- 計算出字符串的三個哈希值(一個用來確定位置,另外兩個用來校驗)
- 察看哈希表中的這個位置
- 哈希表中這個位置為空嗎?如果為空,則肯定該字符串不存在,返回
- 如果存在,則檢查其他兩個哈希值是否也匹配,如果匹配,則表示找到了該字符串,返回
- 移到下一個位置,如果已經越界,則表示沒有找到,返回
- 看看是不是又回到了原來的位置,如果是,則返回沒找到
- 回到3
怎么樣,很簡單的算法吧,但確實是天才的idea, 其實最優秀的算法往往是簡單有效的算法,Blizzard被稱為最卓越的游戲制作公司,不愧于此。(轉載注:這種解決hash collision的方法相對于用linked list方法的缺點在于,hash表的entry只能代表一個字符串,如果hash表滿了則無法在向hash表中加入新的entry)
應用參見魔獸哈希算法封裝和測試
魔獸打包管理器源碼
暴雪公司經典字符串hash公式測試