你有一個非常大的字符串數組A,現在又有一個字符串B,需要你去檢測B是否存在於A中。最簡單粗暴的方法是遍歷整個A,但是這個方法投入到實際應用時的運行速度是難以接受的。在沒有與其他所有字符串比較前怎么知道該字符串是否存在呢?
解決方法是使用哈希表,即用較小的數據類型來代表較大的數據類型,例如:用數字來代表字符串。你可以存儲哈希值與字符串一一對應,當需要檢測一個字符串時,就用哈希算法計算其哈希值,然后與存儲的哈希值比較級可以得出結果,使用這一方法根據數組的大小和字符串長度提升速度大約100倍。
unsigned long HashString(char *lpszString) { unsigned long ulHash = 0xf1e2d3c4; while (*lpszString != 0) { ulHash <<= 1; ulHash += *lpszString++; } return ulHash; }
上述代碼演示了一個簡單的哈希算法。該函數在遍歷整個字符串時,將ulHash左移一位再叫上字符值。使用這個算法 ,"arr\units.dat" 的哈希值是0x5A858026,字符串"unit\neutral\acritter.grp" 的哈希值是0x694CD020。但是這個算法沒有什么使用價值,因為它產生的哈希值是可以預測的,可能使不同的字符產生相同的哈希值,從而產生碰撞。
要解決這一問題方法,網上流傳最神的是MPQ,源自於暴雪公司的文件打包管理,用於Blizzard游戲的數據文件,包括圖形,聲音,等級等數據,該算法能夠壓縮,解密,文件分割等功能。詳情見維基:http://en.wikipedia.org/wiki/MPQ。
該算法產生的哈希值完全無法預測,非常高效,被稱為"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)。
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; }
嘗試在前面的示例中使用相同索引,您的程序一定會有中斷現象發生,而且不夠快。如果想讓它更快,您能做的只有讓程序不去查詢數組中的所有散列值。或者您可以只做一次對比就可以得出在列表中是否存在字符串。聽起來不錯,真的么?不可能的啦
一個哈希表就是以字符串的哈希值作為下標的一類數組。我的意思是,哈希表使用一個固定長度的字符串數組(比如1024,2的偶次冪)進行存儲;當你要看看這個字符串是否存在於哈希表中,為了獲取這個字符串在哈希表中的位置,你首先計算字符串的哈希值,然后哈希表的長度取模。這樣如果你像上一節那樣使用簡單的哈希算法,字符串"arr\units.dat" 的哈希值是0x5A858026,偏移量0x26(0x5A858026 除於0x400等於0x16A160,模0x400等於0x26)。因此,這個位置的字符串將與新加入的字符串進行比較。如果0X26處的字符串不匹配或不存在,那么表示新增的字符串在數組中不存在。下面是示意的代碼:
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
}
上面的說明中存在一個缺陷。當有沖突(兩個不同的字符串有相同的哈希值)發生的時候怎么辦?顯而易見的,它們不能占據哈希表中的同一個位置。通常的解決辦法是為每一個哈希值指向一個鏈表,用於存放所有哈希沖突的值;
MPQs使用一個存放文件名的哈希表來跟蹤文件內部,但是表的格式與通常方法有點不同,首先不像通常的做法使用哈希值作為偏移量,存儲實際的文件名。MPQs 根本不存儲文件名,而是使用了三個不同的哈希值:一個用做哈希表偏移量,兩個用作核對。這兩個核對的哈希值用於替代文件名。當然從理論上說存在兩個不同的文件名得到相同的三個哈希值,但是這種情況發送的幾率是:1:18889465931478580854784,這應該足夠安全了。
MPQ's的哈希表的實現與傳統實現的另一個不同的地方是,相對與傳統做法(為每個節點使用一個鏈表,當沖突發生的時候,遍歷鏈表進行比較),看一下下面的示范代碼,在MPQ中定位一個文件進行讀操作:
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),nHashA = HashString(lpszString, HASH_A),nHashB = HashString(lpszString, HASH_B), 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步;
如果你注意的話,可能已經從我們的解釋和示例代碼注意到,MPQ的哈希表已經將所有的文件入口放入MPQ中;那么當哈希表的每個項都被填充的時候,會發生什么呢?答案可能會讓你驚訝:你不能添加任何文件。有些人可能會問我為什么文件數量上有這樣的限制(文件限制),是否有辦法繞過這個限制?就此而言,如果不重新創建MPQ 的項,甚至無法調整哈希表的大小。這是因為每個項在哈希表中的位置會因為跳閘尺寸而改變,而我們無法得到新的位置,因為這些位置值是文件名的哈希值,而我們根本不知道文件名是什么。
如果想要深入了解MPQ入此坑: http://sfsrealm.hopto.org/inside_mopaq/index.htm