散列表是實現字典操作的一種有效數據結構。儘管在最壞情況下散列表查找一個元素的與鏈表中的時間相同,達到θ(n)。然而在實際應用中,在一些合理的假設下,在散列表中查找一個元素的平均時間是O(1)。
散列表是普通數組概念的推廣。當實際存儲的關鍵字數目比全部的可能關鍵字總數要小時,採用散列表稱爲直接數組尋址的一種有效替代,因爲散列表使用一個長度與實際存儲的關鍵字數目成比例的數組來存儲。在散列表中,不是直接把關鍵字作爲數組的下標,而是根據關鍵字計算出相應的下標。
11.1 直接尋址表
當關鍵字的全域U比較小是,直接尋址是一種簡單而有效的技術。假設某應用要用到一個動態集合,其中每個元素都是取自於全域U={0,1,…,m-1}中的一個關鍵字,m不是一個很大的數。另外,假設沒有兩個元素具有相同的關鍵字。
爲表示動態集合,用一個數組,或稱爲直接尋址表,記爲T[0..m-1],其中,每個位置,或稱爲槽,對應全域U中的一個關鍵字。槽k指向集合彙總一個關鍵字爲k的元素。如果該集合中沒有關鍵字爲k的元素,則T[k] = NIL。
幾個字典操作實現非常簡單:
DIRECT-ADDRESS-SELECT(T, k)
return T[k]
DIRECT-ADDRESS-INSERT(T,x)
T[x.key] = x
DIRECT-ADDRESS-DELETE(T,x)
T[x.key] = NIL
對於某些應用,直接尋址表本身就可以存放動態集合中的元素。使用對象內的一個特殊關鍵字表明該槽爲空槽。而且,通常不必存儲該關鍵字的關鍵字屬性。
11.2 散列表
直接尋址技術的缺點:如果全域U很大,則在一臺計算機可用內存容量中,要存儲大小爲U的一張表T不太實際。還有,實際存儲的關鍵字集合K相對U來說可能很小,使得分配給T的大部分空間都將浪費掉。
當存儲在字典中的關鍵字集合K比所有可能的關鍵字的全域U要小許多時,散列表需要的存儲空間比直接尋址表少得多。同時散列表中查找一個元素的優勢得到保持,只需要O(1),但是是針對平均情況。
在直接尋址方式下,具有關鍵字k的元素被存放在槽K中。在散列方式下,該元素存放在槽h(k)中:即利用散列函數**h,由關鍵字k計算出槽的位置。函數h將關鍵字的全域U映射到散列表**T[0..m-1]的槽位上:
h:U→{0,1,…,m-1}
這裏散列表的大小m一般比|U|小得多。可以說一個關鍵字k的元素被散列到槽h(k)上,也可說h(k)是關鍵字k的散列值。散列函數縮小了數組下標的範圍,即減小了數組的大小。
存在問題:兩個關鍵字映射到同一個槽中,稱這種情形爲衝突。理想的解決辦法就是避免所有的衝突。但是,完全避免衝突是不可能的。因此,一方面可以通過設計的散列函數來儘量減少衝突的次數,另一方面仍需要有解決可能出現的衝突。
餘下部分介紹一種最簡單的衝突解決方法,稱爲鏈接法。11.4介紹另一種方法稱爲開放尋址法。
通過鏈接法來解決衝突
在鏈接法中,把散列到同一槽中的所有元素都放在一個鏈表中。槽 j 中有一個指針,它指向存儲所有散列到 j 的元素的鏈表的表頭;如果不存在這樣的元素,則槽 j 中爲NIL。
字典操作:
CHAINED-HASH-INSERT(T,x)
insert x at the head of list T[h(x,key)]
CHAINED-HASH-SEARCH(T,k)
search for an element with key k in list T[h(k)]
CHAINED-HASH-DELETE(T,x)
delete x from the list T[h(x,key)]
鏈接法散列的分析
給定一個能存放n個元素的、具有m個槽位的散列表T,定義 T 的裝載因子α爲n/m,即一個鏈的平均存儲元素數。
鏈接法的最壞情況:所有的n個關鍵字都散列到同一個槽中,產生一個長長度爲n的鏈表。這是,最壞情況下查找的時間爲θ(n)。
散列方法的平均性能依賴於所選取的散列函數 h,將所有的關鍵字集合分佈在 m 個槽位上的均勻程度。
定理 11.1 在簡單均勻散列的假設下,對於用鏈接法解決衝突的散列表,一次不成功查找的平均時間爲θ(1+α)。
定理 11.2 在簡單均勻散列的假設下,對於用鏈接法解決衝突的散列表,一次成功查找所需的平均時間爲θ(1+ α)。
如果散列表中槽數至少與表中的元素數成正比,則有n = O(m),從而α = n/m = O(m)/m = O(1).所以,查找操作平均需要常數時間。當鏈表採用雙向鏈表時,插入操作在最壞情況下需要O(1)時間,刪除操作最壞情況需要O(1),因而,全部的字典操作平均情況都可以在O(1)時間內完成。
11.3 散列函數
如何設計散列函數,三種具體方法,其中的兩種(用除法進行散列和用乘法進行散列)本質上屬於啓發式方法,第三種(全域散列)則利用了隨機技術來提供可證明的良好性能。
好的散列函數的特點
應滿足簡單均勻散列假設:每個關鍵字都被等可能地散列到 m 個槽位中的任何一個,並與其他關鍵字已散列到哪個槽位無關。
在實際應用中,常常可以運用啓發式方法來構造性能好的散列函數。設計過程中,可以利用關鍵字分佈的有用信息。好的散列函數應能將這些相近符號散列到相同槽中的可能性最小化。
一種好的方法導出的散列值,在某種程度上應獨立於數據可能存在的任何模式。
最後,注意到散列函數的某些應用可能會要求比簡單的均勻散列更強的性質。
11.3.1 除法散列法
通過取 k 除以 m 的餘數,將關鍵字 k 映射到 m 個槽中的某一個上。當應用除法散列時,應避免選擇 m 的某些值。例如 m 不應爲 2 的冪。
一個不太接近 2 的整數冪的素數,常常是 m 的一個較好的選擇。
11.3.2 乘法散列法
第一步:用關鍵字 k 乘上常數A(0 < A <1),並提取 kA 的小數部分。第二部,用 m 乘以這個值,再向下取整。散列函數爲:
h(k) = ⌊m(kA mod 1)⌋
這裏”kA mod 1“是取 kA 的小數部分,即 kA - ⌊kA⌋。
優點:對m 的選擇不是特別關鍵,一般選擇它爲 2 的某個冪次。最佳的選擇與待散列的數據的特徵有關。Knutb[211]認爲A約等於根號5-1除以2=0.618…
11.3.3 全域散列法
隨機地選擇散列函數,使之獨立與要存儲的關鍵字,這種方法稱爲全域散列。
11.4 開放尋址法
在開放尋址法中,所有的元素都存放在散列表中。也就是說每個表項會包含動態集合的一個元素,會包含NIL。不像鏈接法,這裏既沒有鏈表,也沒有元素存放在散列表外。因此,在開放尋址法中,散列表可能會被填滿,以至於不能插入任何新的元素。
當然也可以將用作鏈接的鏈表存放在散列表未用的槽中,但開發尋址的好處在於它不用指針,而是計算出要存取的槽序列。
爲了使用開放尋址法插入一個元素,需要連續地檢查撒列表,或稱爲探查,直到找到一個空槽來放置待插入的關鍵字爲止。爲了確定要探查那些槽,將散列函數加以擴充,使之包含探查號以作爲其第二個輸入參數。對每一個關鍵字 k,使用開放尋址的探查序列
i = 0
repeat
j = h(k,i)
if T[j] == NIL
T[j] = k
return j
else i = i + 1
until i == m
error "hash table overflow"
過程HASH-SEARCH的輸入爲一個散列表T和一個關鍵字k
HASH-SEARCH(T, k)
i = 0
repeat
j = h(k,j)
if T[j] == k
return j
i = i + 1
until T[j] == NIL or i == m
return NIL
刪除操作比較困難。當從槽 i 中刪除關鍵字時,不能僅將NIL置於其中來標識它爲空。因此,在必須刪除關鍵字的應用中,更常見的做法是採用鏈接法來解決衝突。
做一個均勻散列的假設:每個關鍵字的探查序列等可能地爲m!中排列的任一種。有三種方法用來計算開放尋址法中的探查序列:線性探查、二次探查、和雙重探查。但是這些技術都不能滿足均勻散列的假設,因爲它們能產生的不同探查序列數都不超過
線性探查
給定一個普通的散列函數,稱之爲輔助散列函數,給定一個關鍵字 k,首先探查由輔助函數所給出的槽位T[h’(k)],在探查槽T[h’(k)+1],以此類推,直至T[m-1]。然後,又繞到T[0],T[1],…直到最後T[h’(k)-1]。
線性探查比較容易實現,但是存在一個問題:稱爲一次羣集。隨着連續被佔用的槽不斷增加,平均查找時間也隨之不斷增加。
二次探查
初始探查位置爲T[h’(k)],後續的探查位置要加上一個偏移量,該偏移量以二次的方式依賴於探查序號 i。
雙重散列
是用於開放尋址法的最好方法之一。初始探查位置爲
開發尋址散列的分析
使用開放尋址法,每個槽中至多有隻有一個元素。即α <=1.
定理 11.6 給定一個裝載因子爲α <=1的開放尋址散列表,並假設是均勻散列的,則對於一次不成功的查找,其期望的探查次數至多爲1/(1-α).
推論 11.7假設採用的均勻散列,平均情況下,向一個裝載因子爲α <=1的開放尋址散列表插入一個元素至多需要做1/(1-α)次探查。
定理 11.8 對於一個裝載因子爲α <=1的開放尋址散列表,一次成功查找中的探查期望數至多爲:
11.5 完全散列
使用散列技術通常是個好的選擇,不僅它有優異的平均情況性能,而且當關鍵字集合是靜態是,散列表能提供出色的最快情況性能。所謂靜態,就是指一旦各關鍵字存入表中,關鍵字結合就不再變化了。一種散列方法稱爲完全散列,如果該方法進行查找時,能在最壞情況下用O(1)次完成。
採用兩級的散列方法來設計完全散列方案,在每級上都使用全域散列。
第一級與帶鏈接的散列表基本上是一樣的:利用從某一全域散列函數簇中仔細選出的一個散列函數 h,將 n 個關鍵字散列到 m 個槽中。
然而,採用了一個較小的二次散列表