暴雪的hash算法[翻譯]

  原文來自:http://sfsrealm.hopto.org/inside_mopaq/chapter2.htm#hashes算法

促進歷史進步的大多數契機都是在解決特定問題的過程當中產生的,本文討論一下MPQ格式的合適解決方案。數組

----MPQ是暴雪的一種文本壓縮格式,能夠壓縮包括座標、算法、聲音、動畫、字符串等。安全

HASHS網絡

問題:你可能有一個很是長的字符串數組,如今有一個新字符串,想要判斷該字符串是否在數組中,簡單粗暴的方法是挨個比較,但最大的弊端就是大部分場合下速度慢到不能忍。ide

解決方案:hash,hash是一種較小的數據類型,用來替代其餘的較大的數據類型(一般是字符串),上述的問題能夠存儲字符串的hash到數組中,而後比較新字符串的hash是否在數組中,若是數組中的一個hash匹配了新字符串的hash,該字符串就存在於字符串數組中。hash可以將該比較過程提速約100倍,具體的效率提高依賴於數組的長度和字符串的平均長度,下面是一個簡單的hash算法動畫

unsigned long HashString(char *lpszString)
{

unsigned long ulHash = 0xf1e2d3c4;

while (*lpszString != 0)
{


ulHash <<= 1;
ulHash += *lpszString++;

}

return ulHash;
}
上面的代碼提供了一種簡單的hash算法,累加字符串的全部字符,在累加字符串以前hash的基準值左移一位。這個算法簡單也頗有用,但會生成大量類似的輸出,而且常常是在較小的值域內產生大量衝突,衝突是指兩個不一樣的字符串hash後值同樣。

MPQ格式使用了很是複雜的hash算法來生成所有不可預期的值,事實上該hash算法是一個單向算法,單向算法是指沒法經過hash值反算出原始字符串。下面是這個特別的算法:spa

 

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;
}

 hash表
翻譯

問題:你使用一個相似上面例子的索引,但你的程序要求極快的速度,可是索引並無足夠快,你惟一能讓索引速度變快的方法是不檢索數組中的每個hash值。更或者你能夠只作一次比較就能肯定字符串是否存在於數組的任何一個位置,聽起來爽吧?設計

解決方案:hash表是一種特殊的數組,目標字符串的偏移量是目標字符串的hash值。根據應用程序須要建立指定大小的數組,(好比說1024,一般是2的n次方),想要確認新字符串是否在表裏,爲了獲取新字符串在hash表的位置,將新字符串的hash按照hash表大小進行取模運算,餘數就是新字符串在hash表的偏移量。而後將hash表指定偏移位置的字符串同新增字符串進行比較,若是不存在或者不相等,那麼該字符串沒有存在於hash表也即字符串數組中。代碼以下:指針

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
}

上面的方法有一個明顯的問題,若是發生了衝突即兩個不一樣的字符串hash後的值是同樣的,又該怎麼辦?顯然它們不能在hash表佔用一樣的位置。一個經常使用的解決方式是hash的每個節點再也不表明一個元素,而是一個列表的指針,該列表保存hash值爲該偏移的全部字符串。

 MPQs使用一個文件名稱表來跟蹤文件內部信息,但這個表和普通的hash表並不徹底同樣。首先:不是使用文件名稱的hash值做爲偏移量並存儲真實的文件名來作校驗;MPQs根本不會存儲文件名稱,而是使用三個不一樣的hash值:一個用來作hash表的偏移位置,兩個用來作校驗,兩個用來校驗的hash代替了真實的文件名稱。這樣仍是有可能會出現兩個不一樣的字符串hash到一樣的三個值上,不過這樣的機率很是小,大概是1:18889465931478580854784,誰識數的數一數,我是看不懂了,這樣的機率對每個人來講都是足夠安全的。

另外MPQs和一般的hash實現不同的地方是:沒有爲hash表的每個入口保存一個鏈表,在衝突發生的時候繼續向下找,直到找到一個未被佔用的槽。

下面的代碼描述了MPQs定位一個用來讀的文件的過程

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.計算三個hash值並保存在變量裏。

2.移動到hash偏移量的入口。

3.該入口是否未使用,若是是,中止搜索,返回查找失敗。

4.是否入口的兩個檢驗hash等於計算出來的兩個檢驗hash,若是是,中止搜索並返回該入口。

5.移動到鏈表的下一個入口,若是到了末尾就從新從開始查找。

6.若是查找的偏移量和計算的hash偏移量是同樣的,已經搜索了整個表仍然沒有找到,返回未查找到。

7.從第三步從新開始。

若是留心的話,可能會注意到該表是不可擴展的,若是全部的入口都被佔據,那就不可以插入任何的文件名稱,是的,這個表就是這樣設計的,裝載因子最大爲1.0.甚至該表不能夠動態擴大,由於擴大會致使全部的hash入口失效,而且不能從新生成該表,由於不知道相應的文件名稱。

-----若是隻是爲了一個傳奇的hash,這裏就應該夠了,hash的原理、hash的應用都有足夠的說明。

壓縮

問題:若是你有一個巨大的程序,好比50m,想要經過網絡傳輸,但這是一個很是巨大的下載量,人們也可能沒有足夠的耐心來等待它下載完畢。

解決方案:壓縮。壓縮是將大量數據存儲在較小空間的技術,差很少有上百種壓縮算法,工做方式各不相同。MPQs使用的算法是數據壓縮lib的方式,太過複雜不作介紹,會另開文章介紹。

本文的後半部分和主題關係不大,分篇章翻譯,over。

相關文章
相關標籤/搜索