1. 程式人生 > >hash table

hash table

我們知道,雜湊表是一個固定大小的陣列,陣列的每個元素是一個連結串列(單向或雙向)的頭指標。如果Key一樣,則在一起,如果Key不一樣,則不在一起。雜湊表的查詢是飛快的。因為它不需要從頭搜尋,它利用Key的“雜湊演算法”直接定位,查詢非常快,各種資料庫中的資料結構基本都是它。但帶來的問題是,雜湊表的尺寸、雜湊演算法。
雜湊表的陣列是定長的,如果太大,則浪費,如果太小,體現不出效率。合適的陣列大小是雜湊表的效能的關鍵。雜湊表的尺寸最好是一個質數,最小的質數尺寸是17。

當然,根據不同的資料量,會有不同的雜湊表的大小。對於資料量很時多時少的應用,最好的設計是使用動態可變尺寸的雜湊表,那麼如果你發現雜湊表尺寸太小了,比如其中的元素是雜湊表尺寸的2倍時,我們就需要擴大雜湊表尺寸,一般是擴大一倍。下面的數庫是雜湊表變化尺寸時尺寸大小的一個列表。

12345678910111213141516171819202122232425262728293031 staticintprime_array[]={17,            /* 0 */37,            /* 1 */79,            /* 2 */163,           /* 3 */331,           /* 4 */673,           /* 5 */1361,          /* 6 */2729,          /* 7 */5471,          /* 8 */10949
,         /* 9 */21911,         /* 10 */43853,         /* 11 */87719,         /* 12 */175447,        /* 13 */350899,        /* 14 */701819,        /* 15 */1403641,       /* 16 */2807303,       /* 17 */5614657,       /* 18 */11229331,      /* 19 */22458671,      /* 20 */44917381,      /* 21 */89834777,      /* 22 */179669557,     /* 23 */
359339171,     /* 24 */718678369,     /* 25 */1437356741,    /* 26 */2147483647     /* 27 (largest signed int prime) */};#define PRIME_ARRAY_SIZE  (28)

要使用雜湊表,就一定要用一個雜湊演算法,來確定KEY值,這似乎是個很難的事,下面是一個雜湊演算法:

1234567891011121314 typedefstruct_hTab{hLinks* link;   /* 一個連結串列 */int  num;    /* 成員個數 */int  size;   /* 表的尺寸 */}hTab;staticunsignedintgetHashIndex(hTab*tabPtr,constchar*key){unsignedintha=0;while(*key)ha=(ha*128+*key++)%tabPtr->size;returnha;}

(其中key是一個字串,hTab就是一個雜湊表結構, tabPtr->size是雜湊表陣列的大小)

這個演算法被實施證明是比較不錯的,能夠達到分散資料的效果,如果你有更好的演算法,歡迎和我交流。

============ P2  ======================
打造最快的Hash表
 
最近在網上看到篇文章,一起拜一拜暴雪

先提一個簡單的問題,如果有一個龐大的字串陣列,然後給你一個單獨的字串,讓你從這個陣列中查詢是否有這個字串並找到它,你會怎麼做?

有一個方法最簡單,老老實實從頭查到尾,一個一個比較,直到找到為止,我想只要學過程式設計的人都能把這樣一個程式作出來,但要是有程式設計師把這樣的程式交給使用者,我只能用無語來評價,或許它真的能工作,但…也只能如此了。

最合適的演算法自然是使用HashTable(雜湊表),先介紹介紹其中的基本知識,所謂Hash,一般是一個整數,通過某種演算法,可以把一個字串”壓縮” 成一個整數,這個數稱為Hash,當然,無論如何,一個32位整數是無法對應回一個字串的,但在程式中,兩個字串計算出的Hash值相等的可能非常小,下面看看在MPQ中的Hash演算法

1234567891011121314 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),現在仔細看看這個演算法吧

12345678 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;elsereturn -1; //Error value} 

看到此,我想大家都在想一個很嚴重的問題:”如果兩個字串在雜湊表中對應的位置相同怎麼辦?”,畢竟一個數組容量是有限的,這種可能性很大。解決該問題的方法很多,我首先想到的就是用”連結串列”,感謝大學裡學的資料結構教會了這個百試百靈的法寶,我遇到的很多演算法都可以轉化成連結串列來解決,只要在雜湊表的每個入口掛一個連結串列,儲存所有對應的字串就OK了。

事情到此似乎有了完美的結局,如果是把問題獨自交給我解決,此時我可能就要開始定義資料結構然後寫程式碼了。然而Blizzard的程式設計師使用的方法則是更精妙的方法。基本原理就是:他們在雜湊表中不是用一個雜湊值而是用三個雜湊值來校驗字串。

中國有句古話”再一再二不能再三再四”,看來Blizzard也深得此話的精髓,如果說兩個不同的字串經過一個雜湊演算法得到的入口點一致有可能,但用三個不同的雜湊演算法算出的入口點都一致,那幾乎可以肯定是不可能的事了,這個機率是1:18889465931478580854784,大概是10的 22.3次方分之一,對一個遊戲程式來說足夠安全了。

現在再回到資料結構上,Blizzard使用的雜湊表沒有使用連結串列,而採用”順延”的方式來解決問題,看看這個演算法:

123456789101112131415161718192021 int GetHashTablePos(char *lpszString, MPQHASHTABLE *lpTable, int nTableSize){const int HASH_OFFSET = 0, HASH_= 1, HASH_= 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;elsenHashPos = (nHashPos + 1) % nTableSize;if (nHashPos == nHashStart)break;}return -1; //Error value}

1. 計算出字串的三個雜湊值(一個用來確定位置,另外兩個用來校驗)
2. 察看雜湊表中的這個位置
3. 雜湊表中這個位置為空嗎?如果為空,則肯定該字串不存在,返回
4. 如果存在,則檢查其他兩個雜湊值是否也匹配,如果匹配,則表示找到了該字串,返回
5. 移到下一個位置,如果已經越界,則表示沒有找到,返回
6. 看看是不是又回到了原來的位置,如果是,則返回沒找到
7. 回到3

怎麼樣,很簡單的演算法吧,但確實是天才的idea, 其實最優秀的演算法往往是簡單有效的演算法.