暴雪公司有個經典的字串的hash公式
先提一個簡單的問題,假如有一個龐大的字串數組,然後給你一個單獨的字串,讓你從這個數組中尋找是否有這個字串並找到它,你會怎麼做? 有一個方法最簡單,老老實實從頭查到尾,一個一個比較,直到找到為止,我想只要學過程式設計的人都能把這樣一個程式作出來,但要是有程式員把這樣的程式交給使用者,我只能用無語來評價,或許它真的能工作,但...也只能如此了。 最合適的演算法自然是使用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,http://blog.csdn.net/shanzhizi)來解決問題,雜湊表是一個大數組,這個數組的容量根據程式的要求來定義,例如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) { constint 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
下面用一個靜態數組做一個簡單類比(沒有處理hash衝突):
#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;}
下面是一個類的封裝:
代碼一、類聲明標頭檔/////////////////////////////////////////////////////////////////////////////// Name: HashAlgo.h// Purpose: 使用魔獸Hash演算法,實現索引表的填充和尋找功能。// Author: 陳相禮// Modified by:// Created: 07/30/09// RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $// Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved.// Licence: /////////////////////////////////////////////////////////////////////////////#define MAXFILENAME 255 // 最大檔案名稱長度#define MAXTABLELEN 1024 // 預設雜湊索引表大小//////////////////////////////////////////////////////////////////////////// 測試宏定義,正式使用時關閉#define DEBUGTEST 1//////////////////////////////////////////////////////////////////////////// 雜湊索引表定義typedef struct{ long nHashA; long nHashB; bool bExists; char test_filename[MAXFILENAME]; // ......} MPQHASHTABLE;//////////////////////////////////////////////////////////////////////////// 對雜湊索引表的演算法進行封裝class CHashAlgo{public:#if DEBUGTEST long testid; // 測試之用#endif CHashAlgo( constlong nTableLength = MAXTABLELEN )// 建立指定大小的雜湊索引表,不帶參數的建構函式建立預設大小的雜湊索引表 { prepareCryptTable(); m_tablelength = nTableLength; m_HashIndexTable =new MPQHASHTABLE[nTableLength]; for ( int i =0; i < nTableLength; i++ ) { m_HashIndexTable[i].nHashA =-1; m_HashIndexTable[i].nHashB =-1; m_HashIndexTable[i].bExists =false; m_HashIndexTable[i].test_filename[0] ='\0'; } } void prepareCryptTable(); // 對雜湊索引表預先處理 unsigned long HashString(char*lpszFileName, unsigned long dwHashType); // 求取雜湊值 long GetHashTablePos( char*lpszString ); // 得到在定長表中的位置bool SetHashTable( char*lpszString ); // 將字串散列到雜湊表中 unsigned long GetTableLength(void); void SetTableLength( const unsigned long nLength ); ~CHashAlgo() { if ( NULL != m_HashIndexTable ) { delete []m_HashIndexTable; m_HashIndexTable = NULL; m_tablelength =0; } }protected:private: unsigned long cryptTable[0x500]; unsigned long m_tablelength; // 雜湊索引表長度 MPQHASHTABLE *m_HashIndexTable;}; 二、類實現檔案view plaincopy to clipboardprint?///////////////////////////////////////////////////////////////////////////// // Name: HashAlgo.cpp // Purpose: 使用魔獸Hash演算法,實現索引表的填充和尋找功能。 // Author: 陳相禮 // Modified by: // Created: 07/30/09 // RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $ // Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved. // Licence: ///////////////////////////////////////////////////////////////////////////// #include "windows.h" #include "HashAlgo.h" ////////////////////////////////////////////////////////////////////////// // 預先處理 void CHashAlgo::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 ); } } } ////////////////////////////////////////////////////////////////////////// // 求取雜湊值 unsigned long CHashAlgo::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; } ////////////////////////////////////////////////////////////////////////// // 得到在定長表中的位置 long CHashAlgo::GetHashTablePos(char*lpszString) { const unsigned long HASH_OFFSET =0, HASH_A =1, HASH_B =2; unsigned long nHash = HashString(lpszString, HASH_OFFSET); unsigned long nHashA = HashString(lpszString, HASH_A); unsigned long nHashB = HashString(lpszString, HASH_B); unsigned long nHashStart = nHash % m_tablelength, nHashPos = nHashStart; while ( m_HashIndexTable[nHashPos].bExists) { if (m_HashIndexTable[nHashPos].nHashA == nHashA && m_HashIndexTable[nHashPos].nHashB == nHash) return nHashPos; else nHashPos = (nHashPos +1) % m_tablelength; if (nHashPos == nHashStart) break; } return-1; //沒有找到 } ////////////////////////////////////////////////////////////////////////// // 通過傳入字串,將相應的表項散列到索引表相應位置中去 bool CHashAlgo::SetHashTable( char*lpszString ) { const unsigned long HASH_OFFSET =0, HASH_A =1, HASH_B =2; unsigned long nHash = HashString(lpszString, HASH_OFFSET); unsigned long nHashA = HashString(lpszString, HASH_A); unsigned long nHashB = HashString(lpszString, HASH_B); unsigned long nHashStart = nHash % m_tablelength, nHashPos = nHashStart; while ( m_HashIndexTable[nHashPos].bExists) { nHashPos = (nHashPos +1) % m_tablelength; if (nHashPos == nHashStart) { #if DEBUGTEST testid =-1; #endif returnfalse; } } m_HashIndexTable[nHashPos].bExists =true; m_HashIndexTable[nHashPos].nHashA = nHashA; m_HashIndexTable[nHashPos].nHashB = nHash; strcpy( m_HashIndexTable[nHashPos].test_filename, lpszString ); #if DEBUGTEST testid = nHashPos; #endif returntrue; } ////////////////////////////////////////////////////////////////////////// // 取得雜湊索引表長 unsigned long CHashAlgo::GetTableLength(void) { return m_tablelength; } ////////////////////////////////////////////////////////////////////////// // 設定雜湊索引表長 void CHashAlgo::SetTableLength( const unsigned long nLength ) { m_tablelength = nLength; return; } 三、測試主檔案view plaincopy to clipboardprint?///////////////////////////////////////////////////////////////////////////// // Name: DebugMain.cpp // Purpose: 測試Hash演算法封裝的類,完成索引表的填充和尋找功能的測試。 // Author: 陳相禮 // Modified by: // Created: 07/30/09 // RCS-ID: $Id: treetest.h 43021 2009-07-30 16:36:51Z VZ $ // Copyright: (C) Copyright 2009, TSong Corporation, All Rights Reserved. // Licence: ///////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////// // 測試參數設定宏 #define TESTNUM 32 #include <iostream> #include <fstream> #include "HashAlgo.h" usingnamespace std; ////////////////////////////////////////////////////////////////////////// // 測試主函數開始 int main( int argc, char**argv ) { CHashAlgo hash_test( TESTNUM ); cout <<"取得初始化散列索引表長為:"<< hash_test.GetTableLength() << endl; bool is_success = hash_test.SetHashTable( "test" ); if ( is_success ) { cout <<"散列結果一:成功!"<< endl; } else { cout <<"散列結果一:失敗!"<< endl; } is_success = hash_test.SetHashTable( "測試" ); if ( is_success ) { cout <<"散列結果二:成功!"<< endl; } else { cout <<"散列結果二:失敗!"<< endl; } long pos = hash_test.GetHashTablePos( "test" ); cout <<"尋找測試字串:\"test\" 的散列位置:"<< pos << endl; pos = hash_test.GetHashTablePos( "測試" ); cout <<"尋找測試字串:“測試” 的散列位置:"<< pos << endl; ////////////////////////////////////////////////////////////////////////// // 散列測試 for ( int i =0; i < TESTNUM; i++ ) { char buff[32]; sprintf(buff, "abcdefg%d.", i); is_success = hash_test.SetHashTable(buff); is_success ? cout << buff <<"散列結果:成功!位置:"<< hash_test.testid << endl : cout << buff <<"散列結果:失敗!"<< endl; } system( "pause" ); ////////////////////////////////////////////////////////////////////////// // 尋找測試 for ( int i =0; i < TESTNUM; i++ ) { char buff[32]; sprintf(buff, "abcdefg%d.", i); pos = hash_test.GetHashTablePos( buff ); pos !=-1? cout <<"尋找測試字串:"<< buff <<" 的散列位置:"<< pos << endl : cout << buff <<"存在衝突!"<< endl; } system( "pause" ); return0; }