《暴雪公司有個經典的字符串的
hash
公式》
打造最快的
Hash
表
(
和
Blizzard
的對話
)
開元最近學習了一下
Blizzard
的
MPQ
文件格式,頗有一些心得,其中一條就是對
HastTable
的理解,很想寫出來給大家共享,感謝
Justin Olbrantz
的文章《
Inside MoPaQ
》,大多認識來源於此。
先提一個簡單的問題,如果有一個龐大的字符串數組,然後給你一個單獨的字符串,讓你從這個數組中查找是否有這個字符串並找到它,你會怎麼做?
有一個方法最簡單,老老實實從頭查到尾,一個一個比較,直到找到爲止,我想只要學過程序設計的人都能把這樣一個程序作出來,但要是有程序員把這樣的程序交給用戶,我只能用無語來評價,或許它真的能工作,但
...
也只能如此了。
最合適的算法自然是使用
HashTable
(哈希表),先介紹介紹其中的基本知識,所謂
Hash
,一般是一個整數,通過某種算法,可以把一個字符串
"
壓
縮
"
成一個整數,這個數稱爲
Hash
,當然,無論如何,一個
32
位整數是無法對應回一個字符串的,但在程序中,兩個字符串計算出的
Hash
值相等的可能非常
小,下面看看在
MPQ
中的
Hash
算法
unsigned long HashString(char *lpszFileName, unsigned long dwHashType)
{
unsigned char *key = (unsigned char *)lpszFileName;
unsigned long seed1 = 0x7FED7FED, 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"
,舉個例子,字符串
"unitneutralacritter.grp"
通過這個算法得到的結果是
0xA26067F3
。
是不是把第一個算法改進一下,改成逐個比較字符串的
Hash
值就可以了呢,答案是,遠遠不夠,要想得到最快的算法,就不能進行逐個的比較,通常是構造一個
哈希表
(Hash Table)
來解決問題,哈希表是一個大數組,這個數組的容量根據程序的要求來定義,例如
1024
,每一個
Hash
值通過取模運算
(mod)
對應到數組中的一個位置,這樣,只要比較這個字符串的哈希值對應的位置又沒有被佔用,就可以得到最後的結果了,想想這是什麼速度?是的,是最快
的
O(1)
,現在仔細看看這個算法吧
int GetHashTablePos(char *lpszString, SOMESTRUCTURE *lpTable, int nTableSize)
{
int nHash = HashString(lpszString), nHashPos = nHash % nTableSize;
if (lpTable[nHashPos].bExists && !strcmp(lpTable[nHashPos].pString,
lpszString))
return nHashPos;
else
return -1; //Error value
}
看到此,我想大家都在想一個很嚴重的問題:
"
如果兩個字符串在哈希表中對應的位置相同怎麼辦?
",
畢竟一個數組容量是有限的,這種可能性很大。解決該問題
的方法很多,我首先想到的就是用
"
鏈表
",
感謝大學裏學的數據結構教會了這個百試百靈的法寶,我遇到的很多算法都可以轉化成鏈表來解決,只要在哈希表的每
個入口掛一個鏈表,保存所有對應的字符串就
OK
了。
事情到此似乎有了完美的結局,如果是把問題獨自交給我解決,此時我可能就要開始定義數據結構然後寫代碼了。然而
Blizzard
的程序員使用的方法則是更精妙的方法。基本原理就是:他們在哈希表中不是用一個哈希值而是用三個哈希值來校驗字符串。
中國有句古話
"
再一再二不能再三再四
"
,看來
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)
{
if (lpTable[nHashPos].nHashA == nHashA &&
lpTable[nHashPos].nHashB == nHashB)
return nHashPos;
else
nHashPos = (nHashPos + 1) % nTableSize;
if (nHashPos == nHashStart)
break;
}
return -1; //Error value
}
1.
計算出字符串的三個哈希值(一個用來確定位置,另外兩個用來校驗
)
2.
察看哈希表中的這個位置
3.
哈希表中這個位置爲空嗎?如果爲空,則肯定該字符串不存在,返回
4.
如果存在,則檢查其他兩個哈希值是否也匹配,如果匹配,則表示找到了該字符串,返回
5.
移到下一個位置,如果已經越界,則表示沒有找到,返回
6.
看看是不是又回到了原來的位置,如果是,則返回沒找到
7.
回到
3
怎麼樣,很簡單的算法吧,但確實是天才的
idea,
其實最優秀的算法往往是簡單有效的算法,
Blizzard
被稱爲最卓越的遊戲製作公司,不愧於此。
用一個靜態數組給你簡單模擬一下
:
#include <stdio.h>
#define HASH_TABLE_SIZE 13 //
哈希表的大小應是個質數
struct mapping
{
void *key;
void *data;
} hash_table[HASH_TABLE_SIZE];
unsigned int
RSHash (char *str)
{
unsigned int b = 378551;
unsigned int a = 63689;
unsigned int hash = 0 ;
while (*str)
{
hash = hash * a + (*str++);
a *= b;
}
return (hash & 0x7FFFFFFF);
}
int main ()
{
char *str = "we are the world!";
char *filename = "myfile.txt";
unsigned int hash_offset;
//
初始化哈希表
memset (hash_table, 0x0, sizeof (hash_table));
//
將字符串插入哈希表
.
hash_offset = RSHash (str) % HASH_TABLE_SIZE;
hash_table[hash_offset].key = str;
hash_table[hash_offset].data = filename;
//
查找
str
是否存在於
hash_table.
hash_offset = RSHash (str) % HASH_TABLE_SIZE;
if (hash_table[hash_offset].key)
printf ("string '%s' exists in the file
%s./n", str, hash_table[hash_offset].data);
else
printf ("string '%s' does not exist./n",
str);
return 0;
}
這個沒有哈希衝突處理
,
可以自己實現一下