數據結構與算法(七)—— 散列表結構及其實現和應用

注:本篇內容參考了《Java常用算法手冊》、《大話數據結構》和《算法導論(第三版)》三本書籍。並參考了百度百科。

本人水平有限,文中如有錯誤或其它不妥之處,歡迎大家指正!

 

目錄

1. 散列技術

1.1 直接尋址表

1.2 散列表

1.3 散列技術

1.3.1 簡介

1.3.2 完全哈希

1.4 散列過程

1.5 與其它數據結構的不同

2 散列函數

2.1 好的散列函數原則

2.1.1 計算簡單

2.1.2 散列地址分佈均勻

2.1.3 獨立於數據可能可在的任何模式

2.2 散列函數方法

2.2.1 直接定址法

2.2.2 數字分析法

2.2.3 平方取中法

2.2.4 摺疊法

2.2.5 除留餘數法

2.2.6 隨機數法

2.2.7 除法散列法

2.2.8 乘法散列法

2.2.9 全域散列法

2.2.10 使用總結

3 哈希衝突

3.1 哈希衝突

3.2 衝突解決方法

3.2.1 鏈接法

3.2.2 開放尋址法

3.2.3 再散列法

3.2.4 公共溢出區法

4. 散列表的查找

4.1 查找算法實現

4.1.1 初始化

4.1.2 插入散列

4.1.3 查找

4.2 查找性能

4.2.1 散列函數是否均勻

4.2.2 處理衝突的方法

4.2.3 散列表的裝載因子

5. 常用的散列算法

5.1 MD4

5.2 MD5

5.3 SHA-1及其他

6.  散列表應用

6.1 文件檢驗

6.2 數字簽名

6.3鑑權協議

6.4 實際應用


 

1. 散列技術

在實際應用中,許多應用都需要一種動態集合結構,它至少要支持插入、查找和刪除這樣的字典操作。比如,用於程序語言編譯的編譯器維護了一個符號表,其中元素的關鍵字爲任意字符串,它與程序中的標識符對應。

散列表(Hash Table)是實現字典操作的一種有效數據結構。儘管在最壞情況下,散列表中查找一個元素的時間與鏈表中查找的時間相同,時間複雜度爲O(n)。然而在實際應用中,散列表查找的性能是極好的。在一些合理的假設下,在散列表中查找一個元素的平均時間是O(1)

散列表是普通數組概念的推廣由於對普通數組可以直接尋址,使得能在O(1)時間內訪問數組中的任意位置。若存儲空間允許,可以提供一個數組,爲每個可能的關鍵字保留一個位置,以利用直接尋址技術的優勢。

當實際存儲的關鍵字數量比全部的可能關鍵字總數要小時,採用散列表就成爲直接數組尋址有一種有效替代。因爲散列表使用一個長度與實際存儲的關鍵字數量成比例的數組存儲在散列表中,不是直接把關鍵字作爲數組的下標,而是根據關鍵字計算出相應的下標

然而,在根據關鍵字計算下標時可能會出現衝突,就是常說的哈希衝突。這樣會使多個關鍵字映射到數組的同一個下標,本篇後面會說明如何去解決這種衝突。

【說明】:直接尋址、立即尋址和間接尋址,只是CPU在通過總線與內存交互時的不同交互方法而產生的三種概念。直接尋址就是在指令格式的地址的字段中,直接給出操作數在內存地址,因爲操作數的地址直接給出而不需要經過某種變換,故有此稱謂。間接錄址是相對於直接錄址而言的,指令地址字段的形式地址D不 是操作數的真正地址,而是操作數地址的指標器,或說是D單元的內容纔是操作數的有效地址。立即尋址:編程語言中的一種尋址方式,將操作數緊跟在操作碼後,與操作碼一起放在指令代碼段中,在程序運行時,程序直接調用該操作數,而不需要到其它地址單元中去取相應的操作數,上述的寫在指令中的操作數也稱作立即數。

 

1.1 直接尋址表

當關鍵字的全域U比較小時,直接尋址是一種簡單而有效的技術。假設某應用要用到一個動態集合,其中每個元素都是取自於全域U = {0,1,…,m-1}中的一個關鍵字,這裏m不是一個很大的數。另外,假設沒有兩個元素具有相同的關鍵字。

爲表示動態集合,我們用一個數組,或稱爲直接尋址表(direct-address table),記爲T[0..m-1]。其中每個位置,或稱爲槽(slot)對應全域U中的一個關鍵字。如下圖。槽k指向集合中一個關鍵字爲k的元素。若該集合中沒有關鍵字爲k的元素,則T[k]=NULL。

 

在上圖中,全域U = {0,1,…,9}中的每個關鍵字都對應於表中的一個下標值,由實際關鍵字構成的集合K={2,3,5,8}決定表中的一些槽,這些槽包含元素的指針。而另一些槽包含NULL,用深陰影表示。其中,衛星數據可以認爲是除了關鍵字k以外的其它數據,因爲這裏重點關心的是k,其它數據應該會像衛星一樣圍繞着k走。 

對於某些應用,直接尋址表本身就可以存放動態集合中的元素。也就是說,並不是把每個元素的關鍵字及其衛星數據都放在直接尋址表外部的一個對象中,再由表中某個槽的指針指向該對象,而是直接把該對象存放在表的槽中,從而節省了空間。使用對象內的一個特殊關鍵字來表明該槽爲空槽。而且,通常不必存儲該對象的關鍵字屬性,因爲若知道一個對象在表中的下標,就可以得到它的關鍵字。然而,若不是存儲關鍵字,就必須有某種方法來確定某個槽是否爲空。

 

1.2 散列表

從上面的描述可以看出,直接尋址技術的缺點非常明顯:若全域U很大,則在一臺標準的計算機可用內存容量中,要存儲大小爲|U|的一張表T也許不太實際,甚至是不可能的。還有,實際存儲的關鍵字集合K相對U來說可能很小,使得分配給T的大部分空間都被浪費掉

當存儲在字典中的關鍵字集合K比所有可能的關鍵字的全域U要小許多時,散列表需要的存儲空間要比直接發址表少得多。特別地,能將散列表的存儲需要降至\Theta (|K|),同時散列表中查找一個元素的優勢仍得到保持,只需要O(1)的時間。問題是這個界是針對平均情況時間的,而對直接尋址來說,它是適用於最壞情況時間的。

在直接尋址方式下,具有關鍵字k的元素被放在槽k中。在散列方式下,該元素存放在槽h(k)中;即利用散列函數(hash  function)h,由關鍵字k計算得到槽的位置。這裏函數h將關鍵字的全域U映射到散列表(hash  table)T[0..m-1]的槽位上

h:U\rightarrow \left \{ \left. 0, 1, ..., m-1\right \} \right.

這裏散列表的大小m一般要比|U|小很多。可以說一個具有關鍵字k的元素被散列到槽h(k)上,也可以說h(k)是關鍵字k的散列值。下圖描述了這個基本方法。散列函數縮小了數組下標的範圍,即減少了數組的大小,使其由|U|減小爲m

散列是一種極其有效和實用的技術:基本的字典操作平均只需要O(1)的時間。

 

1.3 散列技術

1.3.1 簡介

先看一個生活中的例子。以前在上學的時候,如果家長要來學校找一個人,那時學校基本上沒有軟件系統,找起來很麻煩,學校可能是根據登記的名冊一個一個的找,相當於通過名字順序查找學校的人。假如你問的人剛好知道這個人,他就直接告訴你那個人的位置甚至可以帶你去找那個人。這就相當於通過某個函數,沒有遍歷,也沒有比較,使得

這樣我們可以通過查找的關鍵字不需要比較就可以獲得需要的記錄的位置。這就是一種新的存儲技術——散列技術。

散列技術是在記錄的存儲位置和它的關鍵字之間建立一個確定的對應關係f,使得每個關鍵字key對應一個存儲位置f(key)。查找時,根據這個確定的對應關係找到給定值key的映射f(key),若查找集合中存在這個記錄,則必定在f(key)的位置上。

把這種對應關係 f 稱爲散列函數,又稱哈希(Hash)函數。按這個思想,採用散列技術將記錄存儲在一塊連續的存儲空間中,這塊連續存儲空間稱爲散列表或哈希表(Hash table)

 

1.3.2 完全哈希

完美哈希當關鍵字和值是靜態時,可以使得在最差情況下的查詢性能也相當出色。所謂靜態,就是指一旦各關鍵字存入表中,關鍵字集合就不再變化了。實際應用中,有很多地方都用到了靜態的關鍵字集合,比如一種語言的保留字集合,還有一張CD-ROM裏的文件名集合。完美哈希可以在最壞情況下以O(1)查找,且性能非常的出色,下文中有些地方也會提到。其結構如下圖。

利用完全散列技術來存儲關鍵字集合K = {10,22,37,40,52,60,70,72,75}。外層的散列函數爲h(k) = ((ak + b)  mod  p)  mod  m,這裏 a = 3,b = 42, p = 101,m = 9。例如,h(75) = 2,因此,關鍵字75散列到表T的槽2中。一個二級散列表S_{j}中存儲了所有散列表到槽 j 中的關鍵字。散列表S_{j}的大小爲m_{j} = n_{2}^{j},並且相關的散列函數爲h_{j} (key) = ((a_{j}k + b_{j})  mod  p) mod  m_{j}。因爲h_{2}(75) =7,故關鍵字75被存儲在二級散列表S_{2}的槽中。二級散列表沒有衝突,因而查找操作在最壞情況下所需要的時間爲常數。

它採用兩級的散列方法來設計完全散列方案,在每一級上都使用全域散列。第一級和帶鏈表的哈希基本上是一樣的:利用從某一處全域散列函數簇中仔細選出的一個散列函數h,將 n 個關鍵字散列到 m 個槽中。

但爲了確保在第二級上不出現衝突,需要讓散列表S_{j}的大小m_{j}爲散列到槽 j 中的關鍵字n_{j}的平方。儘管m_{j}n_{j}的這種二次依賴看上去可能使得總體存儲需求很大,但通過適當的選擇第一級散列函數,可以將預期使用的總體存儲空間限制爲O(n)。

採用了一個較小的二次散列表S_{j}及相關的散列函數h_{j},而不是將散列到槽 j 中的所有關鍵字建立一個鏈表。利用精心選擇的散列函數h_{j},可以確保在第二級上不出現衝突。

只是第一級在發生衝突後,後面接的不是鏈表,而是一個新的哈希表。後面的哈希結構,可以看到前端存儲了一些哈希表的基本性質:

  1. m 哈希表槽數;
  2. a, b全域哈希函數要確定的兩個值(一般是隨機選擇然後確定下來的),後面跟着哈希表。

爲了保證不衝突,每個二級哈希表的數量是第一級映射到這個槽中元素個數的平方,這樣可以保證整個哈希表非常的稀疏。

需要處理兩個問題:首先要確定如何才能保證第二級散列表中不發生衝突。其次要說明使用總體存儲空間的期望數O(n),這裏包含主散列表和所有的二級散列表所佔的空間。

如果一個全域散列函數類中隨機選出散列函數h,將 n 個關鍵字存儲在一個大小爲m = n^{2}的散列表中,那麼表中出現衝突的概率小於1/2。在《算法導論》中這是一個定量,證明過程這裏不論述了。

 

1.4 散列過程

整個散列過程其實比較簡單,也比較好理解,一共就兩步。

第一步,在存儲時,通過散列函數計算記錄的散列地址,並按此散列地址存儲該記錄

比如要存儲學生小明的信息,關鍵字就是小明的學號123456,當然也可以是名字,但名字可能會有重複的。根據散列函數對小明的學號123456進行計算,得到一個地址3-2-4-8(這裏是舉例,所以地址是自己定義的,其意思是小明的位置是在3年級2班教室的第4排第8列),這個地址就是小明在學校的位置。就像居里夫人,就讓她在化學實驗室,巴頓將軍就在戰場,當然你可讓他在網吧。

第二步,當查找記錄時,我們通過同樣的散列函數計算記錄的散列地址,按此散列地址訪問該記錄。就是上一步在哪存的,這裏就上哪去找,由於存取用的是同一個散列函數,因此結果自然也是相同的。上一步說明小明的地址是3-2-4-8,這裏在查找時會提供小明的學號123456,根據散列函數計算得出小明的位置仍然是3-2-4-8,這樣就找到小明的位置了。

所以說散列技術既是一種存儲方法,也是一種查找方法。散列技術最適合的求解問題是查找與給定值相等的記錄。對於查找來說,簡化了比較過程,效率會大大提高。但萬事有利就有弊,散列技術不具備很多常規結構的能力

比較那種同樣的關鍵字,它能對應很多記錄的情況,卻不適合用散列技術。比如,一個班級幾十個學生,他們的性別有男有女,你用關鍵字“男”去查找,對應的有許多學生的記錄,這顯然是不合適的。只有如用班級學號或身份證號來散列存儲,此時一個號碼唯一對應一個學生才比較合適。

同樣散列表也不適合範圍查找,比如查找一個班級18~22歲的同學,在散列表中沒法進行。想獲得表中記錄的排序也不可能,像最大值、最小值等結果也都無法從散列表中計算出來。

總之,設計一個簡單均勻、存儲利用率高的散列函數是散列技術中最關鍵的問題

 

1.5 與其它數據結構的不同

散列表與線性表、樹、圖結構不同的是,其它幾種結構,數據元素之間都存在某種邏輯關係,可以用連線圖表示出來,而散列技術的記錄之間不存在什麼邏輯關係,它只關鍵字有關聯。因此,散列主要是面向查找的存儲結構。

 

2 散列函數

經過上面的介紹也知道一個散列函數對散列表來說非常重要,那一個好的散列函數有什麼的原則標準呢?又有什麼樣的方法呢?

2.1 好的散列函數原則

什麼樣的散列函數纔算是好的散列函數呢?這裏有幾個原則可以參考:

  1. 計算簡單;
  2. 散列地址分佈均勻;
  3. 獨立於數據可能可在的任何模式。

 

2.1.1 計算簡單

假如設計了一個算法可以保證所有的關鍵字都不會產生衝突,但這個算法需要很複雜的計算,會耗費很多時間,這對於需要頻繁地查找來說,就會大大降低查找的效率了。因此散列函數的計算時間不應該超過其他查找技術與關鍵字比較的時間

 

2.1.2 散列地址分佈均勻

一個好的散列函數應滿足簡單均勻散列假設:每個關鍵字都被等可能地散列到m個槽位中的任何一個,並與其他關鍵字已散列到哪個槽位無關。就是說若對關鍵字集合中的任一個關鍵字,經散列函數映射到地址集合中任何一個地址的概率是相等的。簡單來說,就是儘量讓散列地址均勻分佈在存儲空間中,這樣可以保證存儲空間的有效利用,並減少爲處理衝突而耗費的時間。遺憾的是,一般無法檢查這一條件是否成立,因爲很少能知道關鍵字散列所滿足的概率分佈,而且各關鍵字可能並不是完全獨立的。

有時若知道關鍵字的概率分佈。如各關鍵字都隨機的實數k,它們獨立均勻地分佈於0\leqslant k< 1範圍中,那麼散列函數

就能滿足簡單均勻散列的假設條件。

在實際應用中,常常可以運用啓發式方法來構造性能好的散列函數。在設計過程中,可利用關鍵字分佈的有用信息。如在一個編譯器的符號表中,關鍵字都是字符串,表示程序中的標識符。一些很相近的符號經常會出現在同一個程序中,如pt和pts。好的散列函數應能將這些相近符號散列到相同槽中的可能性最小化

 

2.1.3 獨立於數據可能可在的任何模式

一種好的散列方法導出的散列值,在某種程度上應獨立於數據可能存在的任何模式。例如,“除法散列”用一個特定的素數來除所給的關鍵字,所得的餘數即爲該關鍵字的散列值。假定所選擇的素數與關鍵字分佈中的任何模式都是無關的,這種方法常常可以給出好的結果。

最後,注意到散列函數的某些應用可能會要求比簡單均勻散列更強的性質。例如,可能希望某些很近似的關鍵字具有截然不同的散列值。下面介紹幾種常用的散列函數構造方法。

 

2.2 散列函數方法

如何設計一個好的散列函數呢?這裏總結了《大話數據結構》和《算法導論》兩本書的內容。下面先講的是《大話數據結構》的內容。

2.2.1 直接定址法

直接定址法是直接取關鍵字的某個線性函數的值作爲散列地址,公式如下。這樣的散列函數簡單均勻,也不會產生衝突,但問題是需要事先知道關鍵字的分佈情況,適合查找表比較小且連續的情況。由於這樣的限制,在實際應用中,此方法雖然簡單,但並不常用。

f(key) = a * key + b(a、b爲常數)

若現在要對0~100歲的人口數字統計表,如下表。因爲年齡是數字,可以直接用年齡這個關鍵字作爲地址。此時f(key) = key

又比如我們要統計80年出生年份的人口數,如下表。那麼可以用出生年份這個關鍵字 減去1980來作爲地址。此時f(key) = key - 1980

2.2.2 數字分析法

數字分析法是使用關鍵字的一部分來計算散列位置的方法。通常適合處理關鍵字位數比較大的情況,若事先知道關鍵字的分佈且關鍵字的若干位分佈較均勻,就可以考慮使用此方法

若關鍵字是多位數字,比如11位的手機號,其中前三位是接入號,一般對應不同的運營商公司的子品牌,如130是聯通如意;中間四位是HLR識別號,表示用戶歸屬地;後四位纔是真正的用戶號,如下表。

現在要存儲某家公司員工登記表,可用手機號作爲關鍵字,那極有可能前7位都是相同的,那麼後四位作爲散列地址就是不錯的選擇。若這樣的抽取工作還是容易出現衝突問題的話,還可對抽取出來的數字再進行反轉(如1234轉成4321)、右環位移(如1234轉成4123)、左環位移、甚至前兩個數與後兩個數疊加(如1234轉12+34=46)等方法。總的目的就是爲了提供一個散列函數,能夠合理地將關鍵字分配到散列表的各個位置。

這裏用到了一個關鍵詞——抽取。抽取方法就是使用關鍵字的一部分來計算散列位置的方法,這在散列函數中是經常用到的手段。

 

2.2.3 平方取中法

平方取中法,就是對關鍵字進行平方運算,再取結果中的幾位。它比較適合於不知道關鍵字的分佈,而位數又不是很大的情況。使用此方法時,首先關鍵字是數字,或容易轉成數字。

假設關鍵字是1234,那麼它的平方就是1522756,再提取中間的3位就是227,用做散列地址。

 

2.2.4 摺疊法

摺疊法是將關鍵字從左到右分割成位數相等的幾部分(需要注意最後一部分位數不夠時可以短一些),然後將這幾部分疊加求和,並按散列表表,取後幾位作爲散列地址。運用此方法時一般事先不需要知道關鍵字的分佈,適合關鍵字倍數較多的情況

例如關鍵字是9876543210,散列表表長爲3位,將關鍵字作爲四組987 | 654 | 321 | 0,然後它們疊加求和爲987+654+321+0=1962,再求後3位得到散列地址爲962.

有時可能這還不能夠保證分佈均勻,不妨從一端向另一端來回摺疊後對齊相加。例如將987和321反轉,再與654和0相加,變成789+654+123+0=1566,此時散列地址爲566。

 

2.2.5 除留餘數法

此方法作爲最常用的構造散列函數的方法。對於散列表長爲m的散列函數公式爲:

f(key) = key  mod  p   (p\leq m)

其中,mod是取模(求餘數)的意思。事實上,此方法不僅可以對關鍵字直接取模,也可以在摺疊、平方取中後再取模。

很顯然,此方法的關鍵在於選擇合適的 p,如果 p 選擇的不好,很可能導致衝突,出現同義詞。

例如下表中,對有12個記錄的關鍵字構造散列表時,就用了f(key) = key.mod 12的方法。如29  mod  12 = 5,所以它存儲在下標爲5的位置。

但這也可能存在衝突。比如上面的29=2*12+5,17=12+5,它們的餘數都是5,還有其它很多種情況。本身是因爲數學運算較爲簡單,結果很多時候是一位數,如果關鍵字一多很容易產生衝突。

甚至會出現一些極端情況,如下表中的關鍵字。若讓p = 12的話,就會出現下面的情況,所有的關鍵字都得到了0這個地址數。

此時不選用p = 12, 而是選用 p = 11。如下表所示。這樣就只有12和144衝突了。相對來說要好很多。

所以,若散列表表長爲m,通常 p 爲小於或等於表長(最好接近m)的最小質數或不包含小於20質因子的合數(自然數中除了能被1和本身整除外,還能被其它數【0除外】整除的數)

 

2.2.6 隨機數法

隨機數法是選擇一個隨機法,取關鍵字的隨機函數值作爲它的散列地址。也就是f(key) = random(key)。這裏random是隨機數。當關鍵字的長度不等時,採用這個方法構造散列函數是比較合適的。下面介紹《算法導論》一書中的三種方法。

在《算法導論》一書中介紹了三種方法:用除法進行散列、用乘法進行散列和全域散列。前兩種本質上屬於啓發式方法,第三種則利用了隨機技術來提供可證明的良好性能。

多數散列函數都假定關鍵字的全域爲自然數集N = {0, 1, 2, ...}。因此若所給關鍵字不是自然數,就需要找到一種方法來將它們轉換爲自然數。例如,一個字符串可以被轉換爲按適當的基數符號表示的整數。這樣就可以將標識符ps轉換爲十進制整數對(112, 116),這是因爲在ASC\amalg字符集中,p =112, t = 116。然後以128爲基數來表示,pt即爲(112 x 128) + 116 = 14452。在一特定的應用場全,通常還能設計出其它類似的方法,將每個關鍵字轉換爲一個(可能是很大的)自然數。在後面的內容中,假定所給的關鍵字都是自然數。

 

2.2.7 除法散列法

在除法散列法中,通過取關鍵字k除以m(散列表的大小)的餘數,將關鍵字k映射到m個槽中的某一個上,即散列函數爲:

例如,若散列表的大小m = 12,所給關鍵字k = 100,則h(k)=4。由於只需做一次除法操作,所以除法散列法是非常快的。

在應用除法散列法時,要避免選擇m的某些值。例如,m不應爲2的冪,因爲若m = 2^{p},則h(k)就是k的p個最低位數字。除非已知各種最低p的排列形式爲等可能的,否則在設計散列函數時,最好考慮關鍵字的所有位。

一個不太接近2的整數冪的素數,常常是m的一個較好的選擇。例如,假定要分配一張散列表並用鏈接法解決衝突,表中大約要存放n = 2000個字符串,其中每個字符有8位。若不介意一次不成功的查找需要平均檢查3個元素,這樣分配散列表的大小m = 701。選擇701這個數的原因是,它是一個接近2000 / 3的數但又不接近2的任何次冪的素數。把每個關鍵字視爲一個整數,則散列函數如下:

2.2.8 乘法散列法

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

這裏“kA  mod  1”是取kA的小數部分,即

乘法散列法的一個優點是對 m 的選擇不是特別關鍵,一般選擇它爲2的某個冪次(m = 2^{p},p 爲某個整數),這是因爲哥以在大多數計算器上,按下面所示的方法較容易的實現散列函數

假設某個計算機的字長爲 w 位,而 k 正好可用一個單字表示。限制A爲形如s / 2^{w}的一個分數,其中 s 是一個取自 0< s < 2^{w}的整數。如下圖。先用 w 位整數 s = A \cdot 2^{w}乘上k,其結果是一個2w 位的值r_{1}2^{w} + r_{0},這裏r_{1}爲乘積的高位字,r_{0}爲乘積的低位字。所求的 p 位散列值中,包含了r_{0}的 p 個最高有效位。

散列的的乘法方法,關鍵字 k 的 w 位表示乘上 s = A \cdot 2^{w}的 w 位值。在乘積的低 w 位中, p 個最高位構成了所需要的散列值h(k)

雖然這個方法對任何的A 都適用,但對某些值的效果更好。最佳的選擇與待散列值的數據有特徵有關。Knuth[211]認爲下面的是一個比較理想的值。

A\approx (\sqrt{5} -1)/2 = 0.618 033 988 7...

假設k = 123 456,p = 14,m = 2^{14} = 16384,且 w = 32。依據Knuth的建議,取A 的值形如s/2^{32}的分數,它與(\sqrt{5}-1)/2最爲接近,於是A = 2 654 435 769/2^{32}。那麼,k * s = 327 706 022 297 664 = (76300 * 2^{32}) +17 612 864,從而有r_{1} = 76300和r_{0}=17 612 864。r_{0}的14個最高有效位產生了散列值h(k) = 67。

 

2.2.9 全域散列法

全域散列法(universal  hashing)就是隨機的選擇散列函數,使之獨立於要存儲的關鍵字。當然這裏不是每次計算時都隨機選擇散列函數,這樣會導致查找時不知道所用的散列函數。而是在構建一個哈希表時隨機選擇一個散列函數,選定之後這個哈希表的所有操作都是基於這個散列函數。這樣即使選擇了怎麼樣的關鍵字,平均性能較好。可以防止惡意的對手來針對某個特定的散列函數選擇要散列的關鍵字,那可以將 n 個關鍵字全部散列到同一槽中,使得平均的檢索時間爲O(n),這是一種令人恐怖的最壞情況。換句話說,全域散列法因爲可以隨機的選擇散列函數,所以在一定程序上可以防止對手的惡意操作導致衝突。

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

\mu爲一組有限散列函數,它將給定的關鍵字全域U映射到{0, 1, ..., m-1}中。這樣的一個函數組稱爲全域的(universal),若對每一對不同的關鍵字 k,l\in U,滿足h(k) = h(l)的散列函數h\in \mu的個數至少爲\mid \mu \mid/ m,這裏可以叫做全域哈希。換句話,若從\mu中隨機選擇一個散列函數,當關鍵字k\neq l時,兩者發生衝突的概率不大於1/m,這也正好是從集合{0, 1, ..., m-1}中獨立地隨機選擇h(k)h(l)時發生衝突的概率。

下面的定理證明,全域散列函數的平均性能是比較好的。注意n_{i}表示鏈表T[i]的長度。

定理    如果h選自一組全域散列函數,將n個關鍵字散列到一個大小爲m的表T中,並用鏈表鏈接法解決衝突。若關鍵字k不在表中,則k被散列至其中的鏈表的期望長度E[n_{h(k)}]至多爲a = n/m。若關鍵字k在表中,則包含關鍵字k的鏈表的期望長度E[n_{h(k)}]至多爲1+a個。

 上圖是證明過程。下面來說明如何設計一個全域散列函數類。

設計一個全域散列函數類很容易,只需要一點數論方面的知識。首先選擇一個足夠大的素數p,使得每一個可能的關鍵字k 都落在0到p-1的範圍內(包含0和p-1)。設Z_{p}表示集合{0,1,...,p-1},\bg_black \large Z_{p}^{\cdot }表示集合{1,2,...,p-1}。由於p是一個一素數,故可以用模運算方法來求解模 p 的方程。假定關鍵字全域的大小大於散列表的槽數,故有p > m。

現在,對於任何\bg_white \large a\in Z_{p}^{\cdot }\bg_white \large b\in Z_{p},定義散列函數\bg_white \large h_{ab}。利用一次線性變換,再進行模 p 和模 m 的歸約,有
                                                   \large h_{ab}(k) = ((ak + b)modp)modm

例如,若 p = 17和 m = 6,則有h_{3.4}(8) = 5。所有這樣的散列函數構成的函數簇爲

                                       \mu _{pm} = { h_{ab}:a\in Z_{p}^{\cdot },b\in Z_{p} }

每一個散列函數h_{ab}都將Z_{p}映射到Z_{m}。這一類散列函數具有一個良好的性質,即輸出範圍的大小 m 是任意的,不必是一個素數。由於對 a 來說有 p-1種選擇,對 b 來說有 p 種選擇,故\mu _{pm}中包含p(p-1)個散列函數。

 

2.2.10 使用總結

其實工作中會經常遇到一些字符串,那它是如何處理的呢?其實無論是英文字符,還是中文字符,也包括各種各樣的符號,它們都可以轉化爲某種數字來處理。比如ASC\amalg碼或Unicode碼等,因此也就可以使用上面的這些方法了。

在實際應用中,應該視不同的情況採用不同的散列函數。下面給出一些考慮因素以提供參數,綜合下面的這些因素,才能決策選擇哪種散列函數更合適。同時也可以幾種方法混合適合。

  1. 計算散列地址所需要的時間;
  2. 關鍵字的長度;
  3. 散列表的大小;
  4. 關鍵字的分佈情況;
  5. 記錄查找的頻率。

最後的三種方法結合了一定的理論基礎,較爲深入,但需要一定的功力去理解。前面的幾種方法是《大話數據結構》一書中講的幾種方法,相對來說,比較通俗易懂,白話形式的講解,很容易去接受。

 

3 哈希衝突

在散列過程中,可能會出現中衝突,即多個關鍵字映射到同一個槽中,這個問題是比較嚴重的。但也要知道一點,設計的再好的散列函數,也不定可以完全避免衝突。但仍然要去解決衝突,並儘量預防這樣的問題。

 

3.1 哈希衝突

若兩個關鍵字可能映射到同一個槽中,稱這種情況爲衝突(collision)。幸運的是,能找到有效的方法來解決衝突。當然,理想的解決方法是避免所有的衝突。

可以試圖選擇一個合適的散列函數h來做到這一點。一個想法就是使h儘可能的“隨機”,從而避免衝突或者使衝突的次數最小化。實際上,術語“散列”的原意就是隨機混雜和拼湊,即體現了這種思想。當然,一個散列函數 h 必須是確定的,因爲某一個給定的輸入 k 應始終產生相同的結果h(k)。但由於|U| > m,故至少有兩個關鍵字其散列值相同,所以要想完全避免衝突是不可能的。因此一方面可以通過精心設計的散列函數來儘量減少衝突的次數,另一方面仍需要有解決可能出現衝突的辦法。

通俗一點說,就是在理想的情況下,每一個關鍵字,通過散列函數計算出來的地址都是不一樣的,然而現實中,仍然有衝突。常會碰到兩個關鍵字key1\neq key2,但卻有f(key1) = f(key2),這種現象稱爲衝突(collision),並把 key1key2稱爲這個散列函數的同義詞(synonym)。出現了衝突當然非常糟糕,將造成數據查找錯誤。儘管我們可以通過精心設計的散列函數讓衝突盡中可能的少,但還是不能完全避免。於是如何解決衝突就成了一個非常重要的問題。

下面介紹兩種解決衝突的辦法:鏈接法(chaining)和開放尋址法(open  addressing)

 

3.2 衝突解決方法

3.2.1 鏈接法

在鏈接法中,把散列到同一個槽中的所有元素都放在一個鏈表中,叫也鏈路法、鏈地址法。如下圖所示。槽 j 中有一個指針,它指向存儲所有散列到 j 的元素的鏈表的表頭;若不存在這樣的元素,則槽 j 中爲NIL。比如每個散列地址設置一個單鏈表,這樣就不存在衝突換地址的問題,無論有多少個衝突,都只是在當前位置給單鏈表增加結點的問題。當然也可以使用雙鏈表等,這樣就把衝突轉換成增加鏈表結點的問題了。前面介紹過鏈表,知道這樣的處理就帶來了查找時需要遍歷鏈表而產生的性能損耗。

插入操作的最壞情況運行時間爲O(1)。插入過程在某種程度上要快一些,因爲假設待插入的元素 x 沒有出現在表中;若需要可以在插入前執行一個搜索來檢查這個假設(需付出額外代價)。查找操作的最壞情況運行時間與表的長度成正比。

若散列表中的鏈表是雙向鏈表,則刪除一個元素 x 的操作可在O(1)的時間內完成。注意到,刪除元素 x 而不是它的關鍵字k作爲輸入,所以無需先搜索 x。若散列表支持刪除操作,爲了能更快的刪除某一元素,應該將其鏈表設計爲雙向鏈表。若是單鏈表,爲了刪除元素 x 要先在表T[h(x, key)]中找到元素 x,然後通過更改 x 的前驅元素的屬性,把 x 從鏈表中刪除。在單鏈表情況下,刪除和查找操作的漸近運行時間相同。

通過鏈接法解決衝突,每個散列表槽T[j] 都包含一個鏈表,其中所有關鍵字的散列值均爲 j 。如h(k_{1})=h(k_{4})。這個鏈表可能是單鏈表,也可能是雙鏈表。上圖中鏈表爲雙鏈表,因爲刪除操作比較快。下面分析下采用鏈接法後散列的性能。

給定一個能存放n個元素的、具有m個槽位的散列表T,定義T的裝載因子(load  factor)a = n / m,即一個鏈的平均存儲元素數。後面的分析將藉助a來說明,a可以小於、等於或大於1

用鏈接法散列的最壞情況性能很差:所有的n個關鍵字都散列到同一個槽中,從而產生出一個長度爲n的鏈表。此時最壞情況下查找的時間爲O(n),再加上計算散列函數的時間,如此就和用一個鏈表來鏈接所有元素差不多了。顯然,並不是因爲散列表的最壞情況性能差,就不使用它。當公完全散列能夠在關鍵字集合爲靜態時,能提供比較好的最壞情況性能。

散列方法的平均性能依賴於所選取的散列函數h,將所有的關鍵字集合分佈在m個槽位中的任何一個。先假設任何一個給定元素等可能的散列到m個槽位中的任何一個,且與其他元素被散列到什麼位置上無關。我們稱這個假設爲簡單均勻散列(simple  uniform  hashing)

對於 j = 0,1,...,m-1,列表T[j]的長度用n_{j}表示,於是有

n = n_{0} + n_{1} + ... + n_{m-1}

並且n_{j}的期望值爲E[n_{j}] = a = n / m

假定可以在O(1)時間內計算出散列值h(k),從而查找關鍵字爲k的元素的時間線性的依賴於表T[h(k)]的長度n_{h(k)}。先不考慮計算散列函數和訪問槽h(k)的O(1)時間,來看看查找算法查找元素的期望數,即爲比較元素的關鍵字是否爲k而檢查表T[h(k)]中的元素數。分兩種情況來考慮。在第一種情況中,查找不成功:表中沒有一個元素的關鍵字爲k。在第二種情況中,成功的查找到關鍵字爲k的元素。

定理1  在簡單均勻散列的假設下,對於用鏈接法解決衝突的散列表,一次不成功查找的平均時間爲\Theta (1+a)

定理2   在簡單均勻散列的假設下,對於用鏈接支解決衝突的散列表,一次成功查找所需的平均時間爲\Theta (1+a)

上面的分析意味着,若散列表中槽數至少與表中的元素數成正比,則有 n = O(m),從而 a = n / m = O(m) / m = O(1)。所以查找操作平均需要常數時間。當鏈表採用雙向鏈接時,插入操作在最壞情況下需要O(1)的時間,刪除操作最壞情況下也需要O(1)的時間,因而,全部的字典操作平均情況下都可以在O(1)時間內完成。

例如,將所有關鍵字爲同義詞的記錄存儲在一個單鏈表中,稱這種表爲同義詞子表,在散列表中存儲所有同義詞子表的頭指針。對於關鍵字集合{12,67,56,16,25,37,22,29,15,47,48,34},表中個數爲12,以12爲除數,進行除留餘數法,可以得到下圖所示的結構。這樣就不存在衝突換地址的問題,無論有多少個衝突,都只是在當前位置給單鏈表增加結點的問題。

 

3.2.2 開放尋址法

開放尋址法(open  addressing),也叫開放定址法,是指一旦發生了衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄記入。這樣的話,所有的元素都放在散列表中。即每個表項要不包含動態集合的一個元素,要不包含空。當查找某個元素時,要系統地檢查所有的表項,直到找到所需要的元素,或者最終查明該元素不在表中

爲了使用開放尋址法插入一個元素,需要連續地檢查散列表,或稱爲探查(probe),直到找到一個空槽來放置待插入的關鍵字爲止。檢查的順序不一定是0,1,...,m-1(這種順序下的查找時間爲O(n)),而是要依賴於待插入的關鍵字。爲了確定要探查哪些槽,將散列函數加以擴充,使之包含探查號(從0開始)以作爲其第二個輸入參數。這樣散列函數就變爲:

           h:U * {0, 1, ..., m-1\rightarrow {0, 1, ..., m-1}

對每個關鍵字 k,使用開放尋址法的探查序列(probe  sequence)h(k,0),h(k,1),...,h(k,m-1),是0,1,...,m-1的一個排列,使得當散列表逐漸填滿時,每一個表位最終都可以被考慮爲用來插入新關鍵字的槽。

在《大話數據結構》中,定義它的公式是(輔助散列函數):

f_{i}(key) = (f(key) + d_{i})  mod  m (d_{i} =1,2,3,...,m-1)

例如,關鍵字集合爲{12,67,56,16,25,37,22,29,15,47,48,34},表長爲12。用散列函數

f(key) = key  mod 12。當計算前5個數{12,67,56,16,25}時,都是沒有衝突的的散列地址,所以是直接存入,如下表。

當計算key = 37時,發現 f (37) = 1,此時與25所在的位置衝突了。於是應用上面的公式f (37) = (f (37) + 1)   mod  12 = 2,可以直接存入,於是將37存入下標爲2的位置。接下來的22,29,15,47和都沒有衝突,正常存入。如下表。

 到了key = 48,計算得到f (48) = 0,與12所在的位置衝突了,於是用上面的公式 f (48) = (f(48) + 1)  mod  12 = 1,此時又與25所在的位置衝突,於是再用上面的公式 f (48) = (f(48) + 2)  mod  12 = 2,還是衝突了...,一直到 f (48) = (f(48) + 6)  mod  12 = 6時纔有空位,此時存入。

把這種解決衝突的開放尋址法,稱爲線性探測法。 這種本來都不是同義詞卻需要爭奪一個地址的情況,稱這種現象爲堆積。堆積的出現,使得需要不斷的處理衝突,無論是存入還是查找效率都會大大降低。

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

 

二次探測法

考慮深一步,若發生這樣的情況:當最後一個key = 34,f (key) = 10,與22所在的位置衝突,可是22後面沒有位置了,反而它的前面有一個空位置,儘管可以不斷的求餘數後得到結果,但效率很差。因此我們可以改進d_{i} = 1^{2},-1^{2},2^{2},-2^{2},...,q^{2},-q^{2},(q\leq m/2),這樣就等於是可以雙向尋找可能的位置。對於34來說,我們取 d_{i}=-1 就可以找到空位置了。另外增加平方運算的目的是爲了不讓關鍵字都聚焦在某一區域。稱這種方法爲二次探測法

f_{i}(key) = (f(key) + d_{i})  mod  m  (d_{i} = 1^{2},-1^{2},2^{2},-2^{2},...,q^{2},-q^{2},q\leq m/2)

上面的公式是在《大話數據結構》中定義的,而在《算法導論》中給出的公式如下:

h(k, i) = (h^{'}(k)+c_{1}i+c_{2}x^{2})   mod   m

其中h^{'}是一個輔助散列函數,c_{1}c_{2}爲正的輔助常數,i = 0,1,...,m-1。初始的探查位置爲T[h^{'}(k)]後續的探查位置要加上一個偏移量,該偏移量以二次的方式依賴於探查序號 i這種探測方法的效果要比線性探測好得多。但爲了能充分利用散列表,c_{1}c_{2}和 m 的值要受到限制。此外,若兩個關鍵字的初始探查位置相同,那它們的探查序列也是相同的,這是因爲h(k_{1},0)=h(k_{2},0)蘊涵着h(k_{1},i)=h(k_{2},i)。這一性質可導致一種輕度的羣集,稱爲二次羣集(secondary  clustering)。像在線性探測中一樣,初始探查位置決定了整個序列,這樣也僅有 m 個不同的探查序列被用到。

可以認爲《大話數據結構》給的公式(上面一個公式)是《算法導論》中公式的一種特殊形式,更容易理解和使用。

 

雙重散列

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

h(k,i)=(h_{1}(k)+ih_{2}(k))   mod   m

其中,h_{1}h_{2}均爲輔助散列函數。初始探查位置爲T[h_{1}(k)],後續的探查位置是前一個位置加上偏移量h_{2}(k)和 m。因此不像線性探查法或二次探查,這裏的探查序列以兩種不同方式依賴於關鍵字 k ,因爲初始探查位置、偏移量或者二者都可能發生變化。下圖給出了一個使用雙重散列法進行插入的例子。

爲了能查找整個散列表,值h_{2}(k)必須要與表的大小 m 互素。有一種簡便的方法確保這個條件成立,就是取 m 爲2的冪,並設計一個總產生奇數的h_{2}。另一種方法是取 m 爲素數,並設計一個總是返回較 m 小的正整數的函數h_{2}。例如,可以取 m  爲素數,並取h_{1}(k)=k  mod   mh_{2}(k)=1+(k   mod  m^{'})

其中m^{'}略小於 m (比如m-1)。例如,若k = 123  456, m = 701,m^{'} = 700,則有h_{1}(k)=80,h_{2}(k)=257,可以知道第一個探查的位置是80,然後檢查第257個槽(模 m),直到找到該關鍵字,或者遍歷了所有槽。

在上圖中,此處散列表的大小爲13,h_{1}(k)=k  mod 13,h_{2}(k)=1+(k  mod 11)。因爲14 \equiv1  mod  13,且14 \equiv (3  mod  11),故在探查了槽1和槽5,並發現它們被佔用後,關鍵字14被插入到槽9中。

m 爲素數或者2的冪時,雙重散列法中用到了\Theta (m^{2})種探查序列,而線性探查或二次探查序列中只用了\Theta (m)種,故前者是後兩種方法的一種改進。因爲每對可能的(h_{1}(k)h_{2}(k))都會產生一個不同的探查序列。因此對於 m 的每一種可能取值,雙重散列的性能看起來就非常接近理想的均勻散列的性能。

儘管除素數和2的冪以外的 m 值理論上也能用於雙重散列中,但在實際中,要高效的生產h_{2}(k)確保使其與 m 互素,將變得更加困難。部分原因是這些數據的相對密度\phi (m) / m可能較小。

 

隨機探測法

在《大話數據結構》中提到還有一種一方法,是在衝突時,對於位移量d_{i}採用隨機函數計算得到,稱之爲隨機探測法。可能有人會問,既然是隨機,那在查找的時候不也隨機生成d_{i}嗎?這裏的隨機數其實是僞隨機數。僞隨機數是說,若設置的隨機種子相同,則不斷調用隨機函數可以生成不會重複的數列,在查找時用同樣的隨機種子,它每次得到的數列是相同的,相同的d_{i}當然可以得到相同的散列地址。隨機種子,Random Seed,計算機專業術語,一種以隨機數作爲對象的以真隨機數(種子)爲初始條件的隨機數。一般計算機的隨機數都是僞隨機數,以一個真隨機數作爲初始條件,然後用一定的算法不停迭代產生隨機數。隨機種子是隨機數的初始值。僞隨機數一般是一直不變的隨機數。根據隨機種子得到的隨機數值是不變的,而不加隨機種子直接得到的隨機數是真的隨機數,會不斷變化。

     f_{i}(key) = (f(key) + d_{i})  mod  m  (d_{i}是一個隨機數列,其實是一個僞隨機數序列)

 

開放尋址法只要在散列表未填滿時,總能找到不發生衝突的地址,是我們常用的解決衝突的辦法

開放尋址法不像鏈接法,既沒有鏈表,也沒有元素存放存放在散列表外。因此在此方法中,散列表可能會被填滿,以至於不能插入任何新的元素。該方法導致的一個結果便是裝載因子a絕對不會超過1。開放尋址法的分析也是以散列表的裝載因子a = n/m來表達的,使用此法,每個槽中至多隻有一個元素,因而n \leqslant m,也就意味着a \leqslant 1

當然也可以將用作鏈接的鏈表存放在散列表未用的槽中,但開放尋址法的好處就在於它不用指針,而是計算出要存取的槽序列。於是不用存儲指針而節省的空間,使得可以用同樣的空間來提供更多的槽,潛在地減少了衝突,提高了檢索速度

 

3.2.3 再散列法

對散列表來說,事先準備多個散列函數,如下公式。其中RH_{i}就是不同的散列函數,可以把前面介紹的除留餘數、摺疊、平方取中全部用上。每當發生散列地址衝突時,就換一個散列函數計算,相信總會有一個可以把衝突解決掉。這種方法能夠使得關鍵字不產生聚集。當然這樣也相應的增加了計算的時間

f_{i}(key)=RH_{i}(key)     (i=1,2,...,k)

在查找時,對關鍵字根據散列函數計算得到地址,然後去散列表中查找,如果找到了對應的關鍵字,就不需要再找了。如果沒有找到就換一種散列函數重新計算後再去查找,直到找到爲止。

 

3.2.4 公共溢出區法

此方法就是將衝突的關鍵字放在一個公共的地方,即爲衝突的關鍵字建立了一個公共的溢出區來存放。例如,對於關鍵字集合{12,67,56,16,25,37,22,29,15,47,48,34},表中個數爲12,以12爲除數,進行除留餘數法,會出現衝突。那就將衝突的關鍵字存放在一個公共的溢出區。如下圖所示。

在查找時,對給定值通過散列函數計算出散列地址後,先與基本表的相應位置進行比對,如果相等則說明查找成功;如果不相等,就到公共溢出區的溢出表進行順序查找。如果相對於基本表而言,有衝突的數據很少的情況下,公共溢出區的結構對查找性能來說還是非常高的。 

 

4. 散列表的查找

應用散列後,必須要有相應的查找關鍵字方法。

4.1 查找算法實現

4.1.1 初始化

首先需要定義一個散列表的結構,以及一些相關的常數。其中HashTable就是散列表結構,結構中的元素爲一個動態數組,開始默認散列表的長度即爲數組的長度。數組用來存放關鍵字,散列地址就是關鍵字在數組中的下標,因爲是一個動態數組,就可以動態的分配數組了。

 

4.1.2 插入散列

因爲要進行散列,在插入元素時計算地址,需要定義散列函數,散列函數可以根據不同情況更改算法,假設用除留餘數法。那散列函數的算法內容就是 key % m。因爲可能存在衝突,所以在計算出散列地址後,判斷數組當前位置是否有值,有則說明衝突了,解決衝突後再存入。

例如,要插入的關鍵字集合還是前面的{12,67,56,16,25,37,22,29,15,47,48,34},先根據關鍵字關鍵得出其散列地址。以關鍵字12爲例,其散列地址 addr = 12 % 12 = 0,那麼關鍵字12就存放在數組的第一個位置。但要考慮到衝突的問題,所以當出現衝突時,可以使用開放尋址法中的線性探測,即addr = (addr + 1) % m,如果還是出現衝突,再次進行同樣的處理,即循環使用開放尋址法的線性探測,直到不衝突爲止。

 

4.1.3 查找

散列表存放了元素,後面就會有取元素的時候。在需要時通過散列表查找記錄。先根據關鍵字計算散列地址,然後根據散列地址從數組中取得關鍵字,如果取得的關鍵字與當前關鍵字不相等,則說明出現了衝突,此時根據散列函數中的衝突解決辦法再次計算後查找,如果找不到所說明查找失敗。

 

4.2 查找性能

散列表的查找過程基本上與造表的過程相同。一些關鍵字可以通過散列函數轉換的地址直接找到。散列查找比很多查找的效率都高,因爲它們的時間複雜度爲O(1)。但沒有衝突只是一種理想,在實際應用中,衝突是不可避免的。另一個關鍵字根據散列函數得到的地址上產生了衝突,就需要按照處理衝突的方法進行查找。在上面給出的幾種處理衝突的方法中,產生衝突後的查找仍然是給定值與關鍵碼進行比較的過程。所以對散列表查找效率的量度,依然用平均查找長度(簡稱ALS【Average  Search  Length】,爲確定記錄在查找的表中的位置,需要與給定值進行比較的關鍵字個數的期望值稱爲查找算法在查找成功時的平均查找長度,只是只描述了定義,後面會寫一篇關於查找的博客,再詳細描述)來衡量。散列查找的平均查找長度取決於下面幾方面的因素。

4.2.1 散列函數是否均勻

散列函數的好壞直接影響着出現衝突的頻繁程度,不過由於不同的散列函數對同一級隨機的關鍵字,產生衝突的可能性是相同的,因此可以不考慮它對平均查找長度的影響。

 

4.2.2 處理衝突的方法

相同的關鍵字、相同的散列函數,但處理衝突的方法不同,會使得平均查找長度不同。比如線性探測處理衝突可能會產生堆積,顯然就沒有二次探測法好,而鏈接法處理衝突不會產生任何堆積,因而具有更佳的平均查找性能。

 

4.2.3 散列表的裝載因子

上面介紹了,裝載因子a = 填入表的記錄個數 / 散列表的長度。a 標誌着散列表的裝滿程度。當填入表中的記錄越多,a 就越大,產生衝突的可能性就越大。比如我們前面的例子,若散列表的長度是12,而填入表中的記錄個數爲11,那此時的裝載因了 a= 11 /12 = 0.9167,再填入最後一個關鍵字產生衝突的可能性就非常之大了。即散列表的平均查找長度取決於裝載因子,而不是取決於查找集合中的記錄個數

 

5. 常用的散列算法

講到這裏,不得不提一些著名的散列算法,MD5和SHA-1可以說是目前應用最爲廣泛的散列算法了,這兩種算法都是以MD4爲基礎設計的。下面詳細描述。

5.1 MD4

MD4(RFC 1320)是MT的Ronald  L.Rivest於1990年設計的,MD是Message  Digest的縮寫,大概意思信息摘要。它適用在32位字長的處理器上用高速軟件實現,它是基於32位操作數的位操作來實現的。

 

5.2 MD5

MD5(RFC 1321)是Rivest 於1991年MD4的改進版本。它對輸入仍以512位分組,其輸出是4個32位字的級聯,與MD4相同。MD5比MD4複雜,並且速度較之要慢一點,但更安全,在抗分析和抗差分方面表現更好。

 

5.3 SHA-1及其他

SHA-1是由NIST NSA設計爲同DSA一起使用的,它對長度小於264的輸入,產生長度爲160bit的散列值,因此抗窮舉(brute-force)性更好。它在設計時基於和MD4相同的原理,並且模仿了該算法。

2004年8月17日,在美國加州聖芭芭拉召開的國際密碼大會上,山東大學王小云教授在國際會議上首次宣佈了她及她的研究小組的研究成果——對MD5、HAVAL-128、MD4和RIPEMD等四個著名密碼算法的破譯結果。2005年2月宣佈破解SHA-1密碼。

 

6.  散列表應用

散列表中生活中有很多的實際應用,Java中的HashMap等用到了,還有快速查找,emule,加密等。

6.1 文件檢驗

比較熟悉的檢驗算法有奇偶檢驗和CRC檢驗,這2種檢驗並沒有抗數據篡改的能力,它們一定程序上檢測出數據傳輸中的信道誤碼,但卻不能防止對數據的惡意破壞。

MD5 Hash算法的“數學指紋”特性,使它成爲目前應用最爲廣泛的一種文件完整性檢驗和(Checksum,在數據處理和數據通信領域中,用於檢驗目的地一組數據項的和,它通常以十六進制爲數製表示的形式。通常用來在通信中,尤其是遠距離通信中保證數據的完整性和準確性)算法,不少Unin系統中提供計算MD5  checksum的命令。

 

6.2 數字簽名

Hash算法也是現代密碼體系中的一個重要組成部分。由於非對稱算法(指一個加密算法的加密密鑰和解密密鑰是不一樣的,或者說不能由其中一個密鑰推導出另一個密鑰)的運算速度較慢,所以在數字簽名協議中,單向散列函數(又稱單向Hash函數、雜湊函數,就是把任意長的輸入消息串變化成固定長的輸出串且由輸出串難以得到輸入串的一種函數。這個輸出串稱爲該消息的散列值。一般用於消息摘要,密鑰加密等,常見的單向散列函數有MD5、SHA、MAC、CRC等)扮演了一個重要的角色。對Hash值,又稱“數字摘要”進行數字簽名,在統計上可以認爲與文件本身進行數字簽名是等效的。而且這樣的還有其他的優點。

 

6.3鑑權協議

下面的鑑權協議又被稱作挑戰—認證模式:在傳輸信息是可被偵聽的,但不可被篡改的情況下,這是一種簡單而安全的方法。

 

6.4 實際應用

:下面的內容摘自百度百科。

在emule中,散列也得到了應用。大家都知道emule是基於P2P (Peer-to-peer的縮寫,指的是對等連接的軟件), 它採用了"多源文件傳輸協議”(MFTP,the Multisource FileTransfer Protocol)。在協議中,定義了一系列傳輸、壓縮和打包還有積分的標準,emule 對於每個文件都有md5-hash的算法設置,這使得該文件獨一無二,並且在整個網絡上都可以追蹤得到。

什麼是文件的hash值呢?MD5-Hash-文件的數字文摘通過Hash函數計算得到。不管文件長度如何,它的Hash函數計算結果是一個固定長度的數字。與加密算法不同,這一個Hash算法是一個不可逆的單向函數。採用安全性高的Hash算法,如MD5、SHA時,兩個不同的文件幾乎不可能得到相同的Hash結果。因此,一旦文件被修改,就可檢測出來。

當我們的文件放到emule裏面進行共享發佈的時候,emule會根據hash算法自動生成這個文件的hash值,他就是這個文件唯一的身份標誌,它包含了這個文件的基本信息,然後把它提交到所連接的服務器。當有他人想對這個文件提出下載請求的時候, 這個hash值可以讓他人知道他正在下載的文件是不是就是他所想要的。尤其是在文件的其他屬性被更改之後(如名稱等)這個值就更顯得重要。而且服務器還提供了,這個文件當前所在的用戶的地址,端口等信息,這樣emule就知道到哪裏去下載了。

一般來講我們要搜索一個文件,emule在得到了這個信息後,會向被添加的服務器發出請求,要求得到有相同hash值的文件。而服務器則返回持有這個文件的用戶信息。這樣我們的客戶端就可以直接的和擁有那個文件的用戶溝通,看看是不是可以從他那裏下載所需的文件。

對於emule中文件的hash值是固定的,也是唯一的,它就相當於這個文件的信息摘要,無論這個文件在誰的機器上,他的hash值都是不變的,無論過了多長時間,這個值始終如一,當我們在進行文件的下載上傳過程中,emule都是通過這個值來確定文件。

那麼什麼是userhash呢?道理同上,當我們在第一次使用emule的時候,emule會自動生成一個值,這個值也是唯一的,它是我們在emule世界裏面的標誌,只要你不卸載,不刪除config,你的userhash值也就永遠不變,積分制度就是通過這個值在起作用,emule裏面的積分保存,身份識別,都是使用這個值,而和你的id和你的用戶名無關,你隨便怎麼改這些東西,你的userhash值都是不變的,這也充分保證了公平性。其實他也是一個信息摘要,只不過保存的不是文件信息,而是我們每個人的信息。

那麼什麼是hash文件呢?我們經常在emule日誌裏面看到,emule正在hash文件,這裏就是利用了hash算法的文件校驗性這個功能了,文章前面已經說了一些這些功能,其實這部分是一個非常複雜的過程,在ftp,bt等軟件裏面都是用的這個基本原理,emule裏面是採用文件分塊傳輸,這樣傳輸的每一塊都要進行對比校驗,如果錯誤則要進行重新下載,這期間這些相關信息寫入met文件,直到整個任務完成,這個時候part文件進行重新命名,然後使用move命令,把它傳送到incoming文件裏面,然後met文件自動刪除,所以我們有的時候會遇到hash文件失敗,就是指的是met裏面的信息出了錯誤不能夠和part文件匹配,另外有的時候開機也要瘋狂hash,有兩種情況一種是你在第一次使用,這個時候要hash提取所有文件信息,還有一種情況就是上一次你非法關機,那麼這個時候就是要進行排錯校驗了。

 

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