一、整數的Hash函數
常用的方法有三種:直接取餘法、乘積取整法、平方取中法。下面我們對這三種方法分別進行討論。以下假定我們的關鍵字是,Hash表的容量是,Hash函數為
。
1.直接取餘法
我們用關鍵字 除以
,取餘數作為在Hash表中的位置。函數運算式可以寫成:
例如,表容量 ,關索引值
,那麼 。該方法的好處是實現容易且速度快,是很常用的一種方法。但是如果選擇的不好而偏偏標本又很特殊,那麼資料在Hash中很容易紮堆而影響效率。
對於 的選擇,在經驗上,我們一般選擇不太接近的一個素數;如果關鍵字的範圍較小,我們一般在此範圍1.1~1.6倍範圍內選擇。例如的範圍為,那麼即為一個不錯的選擇。競賽的時候可以寫一個素數產生器或乾脆自己寫一個“比較像素數”的數。
我用4000個數插入一個容量為701的Hash表,得到的結果是:
|
測試資料 |
隨機資料 |
連續資料 |
|
最小單元容量: |
0 |
5 |
|
最大單元容量: |
15 |
6 |
|
期望容量: |
5.70613 |
5.70613 |
|
標準差: |
2.4165 |
0.455531 |
可見對於隨機資料,取餘法的最大單元容量達到了期望容量的將近3倍。經測試,在我的機器(Pentium III 866MHz,128MB RAM)上,該函數的已耗用時間大約是39ns,即大約35個刻度。
2.乘積取整法
我們用關鍵字 乘以一個在
中的實數 (最好是無理數),得到一個之間的實數;取出其小數部分,乘以,再取整數部分,即得在Hash表中的位置。函數運算式可以寫成:
其中 表示 的小數部分,即。例如,表容量,種子(是一個實際效果很好的選擇),關索引值,那麼。
同樣用4000個數插入一個容量為701的Hash表(),得到的結果是:
|
測試資料 |
隨機資料 |
連續資料 |
|
最小單元容量: |
0 |
4 |
|
最大單元容量: |
15 |
7 |
|
期望容量: |
5.70613 |
5.70613 |
|
標準差: |
2.5069 |
0.619999 |
從公式中可以看出,這個方法受 的影響是很小的,在的值比較不適合直接取餘法的時候這個方法的表現很好。但是從上面的測試來看,其表現並不是非常理想,且由於浮點運算較多,運行速度較慢。經反覆最佳化,在我的機器上仍需892ns才能完成一次計算,即810個刻度,是直接取餘法的23倍。
3.平方取中法
我們把關鍵字 平方,然後取中間的位作為Hash函數值返回。由於的每一位都會對其平方中間的若干位產生影響,因此這個方法的效果也是不錯的。但是對於比較小的值效果並不是很理想,實現起來也比較繁瑣。為了充分利用Hash表的空間,最好取2的整數次冪。例如,表容量,關索引值,那麼。
用4000個數插入一個容量為512的Hash表(注意這裡沒有用701,是為了利用Hash表的空間),得到的結果是:
|
測試資料 |
隨機資料 |
連續資料 |
|
最小單元容量: |
0 |
1 |
|
最大單元容量: |
17 |
17 |
|
期望容量: |
7.8125 |
7.8125 |
|
標準差: |
2.95804 |
2.64501 |
效果比我們想象的要差,尤其是對於連續資料。但由於只有乘法和位元運算,該函數的速度是最快的。在我的機器上,一次運算只需要23ns,即19個刻度,比直接取餘法還要快一些。
比較一下這三種方法:
|
|
實現難度 |
實際效果 |
運行速度 |
其他應用 |
|
直接取餘法 |
易 |
好 |
較快 |
字串 |
|
乘積取整法 |
較易 |
較好 |
慢 |
浮點數 |
|
平方取中法 |
中 |
較好 |
快 |
無 |
從這個表格中我們很容易看出,直接取餘法的性價比是最高的,因此也是我們競賽中用得最多的一種方法。
對於實數的Hash函數,我們可以直接利用乘積取整法;而對於標本為其他類型資料的Hash函數,我們可以先將其轉換為整數,然後再將其插入Hash表。下面我們來研究把其他類型資料轉換成整數的方法。
二、字串的Hash函數
字串本身就可以看成一個256進位(ANSI字串為128進位)的大整數,因此我們可以利用直接取餘法,線上性時間內直接算出Hash函數值。為了保證效果,仍然不能選擇太接近的數;尤其是當我們把字串看成一個進位數的時候,選擇會使得該字串的任意一個排列的Hash函數值都相同。(想想看,為什嗎?)
常用的字串Hash函數還有ELFHash,APHash等等,都是十分簡單有效方法。這些函數使用位元運算使得每一個字元都對最後的函數值產生影響。另外還有以MD5和SHA1為代表的雜湊函數,這些函數幾乎不可能找到碰撞(MD5前一段時間才剛剛被破解)。
我從Mark Twain的一篇小說中分別隨機抽取了1000個不同的單詞和1000個不同的句子,作為短字串和長字串的測試資料,然後用不同的Hash函數把它們變成整數,再用直接取餘法插入一個容量為1237的Hash表,遇到衝突則用新字串覆蓋舊字串。通過觀察最後“剩下”的字串的個數,我們可以粗略地得出不同的Hash函數實際效果。
|
|
短字串 |
長字串 |
平均 |
編碼難度 |
|
直接取餘數 |
667 |
676 |
671.5 |
易 |
|
P. J. Weinberger Hash |
683 |
676 |
679.5 |
難 |
|
ELF Hash |
683 |
676 |
679.5 |
較難 |
|
SDBM Hash |
694 |
680 |
687.0 |
易 |
|
BKDR Hash |
665 |
710 |
687.5 |
較易 |
|
DJB Hash |
694 |
683 |
688.5 |
較易 |
|
AP Hash |
684 |
698 |
691.0 |
較難 |
|
RS Hash |
691 |
693 |
692.0 |
較難 |
|
JS Hash |
684 |
708 |
696.0 |
較易 |
把1000個隨機數用直接取餘法插入容量為1237的Hash表,其覆蓋單元數也只達到了694,可見後面的幾種方法已經達到了極限,隨機性相當優秀。然而我們卻很難選擇,因為不存在完美的、既簡單又實用的解決方案。我一般選擇JS Hash或SDBM
Hash作為字串的Hash函數。這兩個函數的代碼如下:
unsigned int JSHash(char *str){unsigned int hash = 1315423911; // nearly a prime - 1315423911 = 3 * 438474637while (*str){hash ^= ((hash << 5) + (*str++) + (hash >> 2));}return (hash & 0x7FFFFFFF);}unsigned int SDBMHash(char *str){unsigned int hash = 0;while (*str){// equivalent to: hash = 65599*hash + (*str++);hash = (*str++) + (hash << 6) + (hash << 16) - hash;}return (hash & 0x7FFFFFFF);}
JSHash的運算比較複雜,如果對效果要求不是特別高的話SDBMHash是一個很好的選擇。