散列表

定義

散列表(Hash Table)也叫哈希表,是根據關鍵字進行數據訪問的數據結構。散列表將關鍵字映射到表中的某個位置來訪問數據,提供映射的叫散列函數(Hash Function)。通過散列函數,我們可以實現對數據的快速訪問(理想情況下是常數時間內,但這取決於散列函數、碰撞概率、負載係數等因素)。

實際上,散列表是普通數組概念的推廣。由於對普通數組可以直接尋址,使得能在O(1)時間內訪問數組中的任意位置。而當實際存儲的關鍵字數目比全部的可能關鍵字總數要小時,採用散列表就成爲直接尋址的一種有效替代,因爲散列表使用一個長度與實際存儲的關鍵字數目成比例(這個比例稱爲負載係數)的數組來存儲。在散列表中,不是直接把關鍵字作爲數組的下標,而是根據關鍵字計算出相應的下標。


基本概念

·關鍵字通過散列函數映射到散列表上的位置稱爲散列地址,或者說散列函數計算出的結果叫散列值。映射過程稱爲散列。

·有兩個關鍵字k1k2k1≠k2,但H(k1)=H(k2),即兩個不同的關鍵字通過散列函數映射到了表中同一位置,這種現象稱爲碰撞(collision),也叫衝突。

·元素個數除以散列表大小得到的值稱爲負載係數或負載因子(load factor),負載係數的範圍永遠是[0, 1],除非使用開鏈(separate chaining)策略。

·若對於關鍵字集合中的任意關鍵字,映射到散列表中的任何位置的概率都是相等的,並與其他關鍵字已散列到什麼位置無關,則稱這個散列函數爲均勻散列函數(Uniform Hash Function),散列函數的設計應該朝着這一方向努力。

散列函數

一個好的散列函數應(近似地)滿足簡單均勻散列假設(上文有提到)。但一般無法檢查這一條件是否成立,因爲很少能知道關鍵字散列所滿足的概率分佈,而且各關鍵字可能並不是完全獨立的。

有時我們能知道關鍵字的概率分佈。例如,如果各關鍵字都是隨機的實數k,它們獨立均勻地分佈於0≤k<1範圍中,那麼散列函數 h(k) = km 就能滿足簡單均勻散列的假設條件。

好的散列函數導出的散列值在某種程度上應獨立於數據可能存在的任何模式,例如除法散列中所選擇的素數應該與關鍵字分佈中的任何模式都是無關的,這樣纔能有好的結果。

另外,散列函數的某些應用可能會要求比簡單均勻散列更強的性質。例如,可能希望某些很近似地關鍵字具有截然不同的散列值。全域散列通常能夠提供這種性質。

除法散列法

在用來設計散列函數的除法散列中,通過取k除以m的餘數,將關鍵字k映射到m個位置中的某一個上。散列函數爲:

h(k) = k mod m

當應用除法散列法時,要注意的是避免選擇某些m值。例如,m不應該爲2的冪,因爲如果m2p,則h(k)就是kp個最低位數字。除非已知各種最低p位的排列形式爲等可能的,否則再設計散列函數時最好考慮關鍵字的所有位。一個不接近2的證書冪的素數通常是好的選擇。

乘法散列法

構成散列函數的乘法散列法包含兩個步驟。第一步,用關鍵字k乘上常數A(0<A<1),並提取kA的小數部分。第二步,用m乘以這個值,再向下取整。散列函數爲:

h(k) = m(kA mod 1) 

“kA mod 1”即爲第一步,它等於kA-kA

乘法散列法對m的選擇不是特別關鍵,一般選擇它爲2的冪。雖然選擇2p作爲m值對於任何A值都適用,但對於某些值效果更好。最佳選擇與待散列函數的特徵有關,一種說法是A≈(√5 - 1)/2=0.618033...是個比較理想的值。

全域散列法

散列的最壞情況是將所有關鍵字都散列到同一個位置中,使得平均檢索時間爲線性。任何一個特定的散列函數都有可能出現這種最壞情況,唯一有效地改進方法是隨機地選擇散列函數,使之獨立於要存儲的關鍵字。這種方法稱爲全域散列(universal hashing)。

全域散列法在執行開始時就從一組精心設計的函數中,隨機地選擇一個作爲散列函數。隨機化保證了沒有哪一種輸入會最終導致最壞性能產生,因爲隨機地選擇散列函數,算法在每一次執行時都會有所不同,甚至對於相同的輸入都會如此。這樣就可以確保對於任何輸入,算法都具有較好的平均情況性能。

開放尋址法

在開放尋址法(open addressing)中,所有元素都存放在散列表裏。也就是說,每個表項要麼爲空,要麼爲動態集合的一個元素。當查找某個元素時,要系統地檢查所有表項,直到找到所需元素,或者最終查明該元素不在表中。因此在開放尋址法中,散列表可能會被填滿,需要擴充。這使得開放尋址法的負載係數永遠不會超過1

開放尋址法插入一個元素需要連續地檢查散列表,或稱爲探查(probe),直到找到一個空位置來插入。爲了確定要探查哪些位置,我們將散列函數加以擴充,使之包含探查號(從0開始)以作爲第二個參數。那麼,使用開放尋址法的探查序列就是:h(k, 0), h(k, 1), h(k, 2),..., h(k, m-1)

更具體化些,根據對探查號的不同處理(利用一個輔助散列函數),開放尋址法又可進一步細分。常見的主要有線性探查、二次探查和雙重散列。

線性探查

給定一個普通的散列函數h',稱爲輔助散列函數,線性探查(linear probing)採用的散列函數爲:

h(k, i) = (h'(k) + i) mod m    i = 0, 1, 2, ..., m-1

線性探查建立散列表的過程是,關鍵字k根據散列函數映射到一個散列地址後,若該地址已經存在元素,則跳到下一個位置,直到找到一個沒有元素的位置爲止。爲了防止映射地址靠後導致前面的空地址無法利用,對錶的探測一般設計成循環模式。

分析線性探查的性能,最好情況是第一次映射到的地址就可以存放,最壞情況是需要探測整個散列表才能找到空的位置,因此其平均性能理論上是探測散列表的一半。但要接近最優性能,必須使得散列表足夠大且元素足夠獨立。但後者的假設太過理想:現實情況是,關鍵字與值之間的多對多映射是很常見的。

線性探查比較容易實現,但存在一個稱爲主羣集(primary clustering)的問題。隨着連續被佔用的位置不斷增加,平均查找時間也隨之增加。這是因爲當一個空位置前有i個滿的位置時,該空位置爲下一個將被佔用的概率是(i+1)/m。連續佔用的位置就會變得越來越長,因而平均探查時間也會越來越大。

二次探查

二次探查quadratic probing採用如下散列函數:

h(k, i) = (h'(k) + c1i + c2i²) mod m

爲了能夠充分利用散列表,c1c2m的值要受到限制。雖然與線性探查相比二次探查有效解決了主羣集問題,但如果兩個不同的關鍵字的初始探查位置都是相同的,那麼他們的探查序列也是相同的,這就會導致二次羣集(secondary clustering)問題,二次羣集問題沒有主羣集嚴重。

雙重散列

雙重散列(double hashing)是用於開放尋址法的最好方法之一,因爲它所產生的排列具有隨機選擇排列的多樣性。雙重散列的散列函數如下:

h(k, i) = (h1(k) + ih2(k)) mod m

其中h1h2均爲輔助散列函數。不像線性探查或二次探查,雙重散列的探查序列以兩種不同方式依賴於關鍵字k,因爲初始探查位置、偏移量或者二者都有可能發生變化,因此可以很好的減少羣集出現的可能性。線性探查和二次探查有m中探查序列,而雙重散列有種,因此後者是前兩者的改進。因爲每一對可能的(h1(k), h2(k))都會產生一個不同的探查序列,因此對於m的每一種可能取值,雙重散列的性能看起來就非常接近理想的均勻散列的性能。

爲了能查找整個散列表,雙重散列要求值h2(k)必須要與表的大小m互素。

分析

對於開放尋址法,基於其散列表的特點,其負載因子(n/m)不可能超過1

假設採用的是均勻散列,開放尋址法的最壞查找情況(不存在合適的散列值)的探查次數至多爲1/(1-n/m),而最好探查次數爲1。其平均探查次數爲1/(n/m)*(1/(1-n/m))。那麼由此可得到:如果散列表是半滿的(即負載係數爲0.5),平均探查次數小於1.387,如果負載係數爲0.9,則平均探查次數小於2.559

鏈接法

在鏈接法(chaining)中,把散列到同一位置的所有元素都放在一個鏈表中,則散列表中存儲的是指向鏈表頭的指針。可以證明,對於鏈接法構造的散列,插入操作的時間是O(1),如果鏈表是雙向鏈接的,刪除操作也可以在O(1)時間內完成。而對於查找操作,這種散列表的平均性能與最壞性能都是1+n/m

哈希桶

哈希桶就是採用鏈接法構造的散列表,之所以叫哈希桶,是因爲它的數據結構如圖所示:




完全散列

完全散列(perfect hashing)採用兩級的散列方法來設計。它對於靜態的關鍵字集合(關鍵字一旦存入表中就不再變化)具有最壞性能爲O(1)的訪存次數。靜態關鍵字集合很常見,一些應用存在着天然的靜態關鍵字集合,如程序設計語言中的保留字集合,或者CD-ROM上的文字集合等。

完全散列的第一級散列表採用全域散列來設計,第一級散列表中存儲的是二級散列函數(通常也是全域散列函數),二級散列表中存儲的纔是數據。關鍵字通過第一級散列(從某一全域散列函數蔟中選出一個散列函數)後找到一級散列表中的對應位置,然後再從全域散列函數蔟中選出一個散列函數進行二次散列,最終得到二次散列表(secondary hash table)中的位置(數據的最終存儲位置)。如圖所示:


《算法導論》中有一種具體實現,一級散列函數爲h(k) = ((ak+b) mod p) mod m,這裏a=3b=42p=1101m=9二級散列函數爲hj(k) = ((ajk+bj) mod p) mod mj


爲了達到最壞情況下查找操作所需時間爲常數,需要確保在第二級上不出現衝突,因此要讓二級散列表的大小爲散列到其中的元素個數的平方。但當元素數目比較大時,二級散列表還是很大的。不過,我們可以通過適當地選擇第一級散列函數(讓n個關鍵字均勻地散落到一級散列表中),使其使用的總體存儲空間限制爲O(n)




本文部分內容摘自《算法導論(中文版)第3版》,有改動

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章