1. 程式人生 > >暴雪公司關於字串匹配的hash演算法

暴雪公司關於字串匹配的hash演算法

暴雪公司有個經典的字串的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;   
}