查找--理解哈希算法並實現哈希表

我們喜歡使用數組進行數據的查找,就是因爲數組是一種“隨機存取”的數據結構,我們根據數組的起始地址和數組元素的下標值就可以直接計算出每一個數組元素的存儲位置,所以它的查找時間是O(1),而與數組的個數無關。

我們在這個思想的基礎上,可以聯想到,如果有一種數據結構,讓我們在進行關鍵字查找的時候,也可以像數組一樣,進行隨機存儲,使其時間複雜度從O(n)降到O(1),那就可以大大提高查找的效率。我們的前輩們基於這種想法發明了散列方法,也就是哈希或關鍵字地址計算方法


基本思想

我們試圖尋找一種關係,可以根據我們要存儲的關鍵字(key)然後使用這種關係直接計算出它應該存儲的位置(p),一旦建立起這種關係,那麼我們在之後一旦需要查找此關鍵字的話,只需計算此對應關係所產生的值就可以直接得到關鍵字所在的地址,那麼查找的時間複雜度也就降到了O(1),我們將剛纔所說的轉換爲一種數學關係:

p(位置)= H(key)

其中H就是對應關係,我們稱之爲哈希函數,p被成爲散列地址。因此,哈希算法的核心就是找到哈希函數(H),通過這個函數來組織存儲並進行查找。


Hash函數的構造方法

我們先來看一個問題:
假如我們現在有一些單詞(關鍵字):and,cell,do,flag … … 等等,如果我們的哈希函數取值爲關鍵字的第一個字母在字母表中的字典順序,那麼關鍵字會依次分佈在散列地址中。然而當我們還使用這套規則進行and,ant,apple,cell,do … …等等字母的存儲,那麼就會產生“地址衝突”的問題,因爲前三個單詞在字母表中的順序是一樣的。

由於Hash函數是一個壓縮映像,因此在實際應用中,很少存在不產生衝突的hash函數,因此,如何構造恰當的Hash函數,使得節點“均勻分佈”,儘量少的產生衝突就是我們必須要解決的問題之一了。

hash函數的構造原則爲簡單和均勻:

  • hash函數本身運算簡單,便於計算;
  • hash函數值必須在散列地址範圍內,且分佈均勻,地址衝突儘可能少。

有幾種常用的hash函數的構造方法:

1.除留餘數法

該方法是最爲簡單常用的一種方法,假設表長爲m(散列地址的長度),p爲小於等於表長m的最大素數,則hash函數爲H(key)= key % p。H就是在散列地址中位置。

p的取值應該是一個質因子,這樣才能減少“地址衝突”的可能性。


2.數字分析法

假設關鍵字集合中的每個關鍵字都是由s位數字組成(k1, k2, … , kn),如果可以預先估計出全體關鍵字的每一位上各種數字出現的頻度時,分析關鍵字集中的全體,並從中提取出分佈均勻的若干位或它們的組合作爲hash地址。

例如:
H(49646542)= 465, H(49673242)= 732 … …

經過分析,各個關鍵字中第4~6位中的取值比較均勻,則hash函數爲H(key)= d4d5d6。


3.平方取中法

由於整數相除的運行速度通常比相乘要慢,所以我們需要有意識的避免使用除餘法可以提高散列算法的運行時間。平方取中法:首先通過求關鍵字的平方值擴大相近數的差別,然後根據表長度取中間的幾位數作爲hash函數值。又因爲一個乘積的中間幾位數和乘數的每一位都相關,因此由此產生的散列地址較爲均勻。


4.分段疊加法

有時關鍵字所含的位數很多,採用平方取中法計算太複雜,則可將關鍵字分割成位數相同的幾部分(最後一部分的位數可以不同),然後取這幾部分進行疊加,疊加和(捨去進位)作爲散列地址。

具體的疊加方式有移位疊加和摺疊疊加


5.基數轉換法

首先將關鍵字看做是另一種進制的數,然後在轉換成原來進制的數,再選擇其中幾位作爲散列地址。

例如:
把十進制(362081)看做13進制的數,最後結果再轉換爲十進制(1289744),假設散列長度是10000,則可取低四位9744作爲散列地址。


一般來說我們還是應該根據實際情況採用恰當的哈希算法,並測試它的性能,一般考慮下列因素:

  • 計算hash函數所需的時間;
  • 關鍵字的長度;
  • 散列表的大小;
  • 關鍵字分佈的情況;
  • 記錄查找的頻率。

處理衝突的方法

在上面我們說過,實際情況中我們是不可能不產生地址衝突的,所以,一旦我們有地址衝突,我們應該怎麼辦?我們自然而然的想到,那就爲產生衝突的地址尋找下一個散列地址。


開放定址(再散列)法

基本思想:
當關鍵字key的初始散列地址h0=H(key)出現衝突時,以h0爲基礎查找下一個地址h1,如果h1仍然衝突,再以h0爲基礎,產生另一個散列地址h2… … 直到找出一個不衝突的地址hi,將相應元素存入其中,這種方法有一個通用的再散列函數形式:

hi=((H(key)+ di)% m

其中h0=H(key),m爲表長,di爲增量序列。增量序列的取值方式不同,對應不同的再散列方式:

1.線性探測再散列

di = c × i

最簡單的情況:c = 1

特點:衝突放生時,順序查看錶中下一個單元,直到找到一個空單元或查遍全表。值得注意的是:由於使用的是%(取餘)運算符,所以它和循環隊列有點相似,表尾的後邊是表頭,表頭的前邊是表尾。


2.二次探測再散列

di = 1^2,-1^2,2^2,-2^2, … … ,k^2,-k^2 (k<=(m/2))

特點:衝突發生時分別在表的右,左進行跳躍式探測,較爲靈活,不易產生聚集,缺點是不能探查到整個散列地址空間。


3.隨機探測再散列

di = 僞隨機數

特點:建立一個隨機數發生器,並給定一個隨機數作爲起始點。


鏈地址法

基本思想:

把所有具有地址衝突的關鍵字鏈在同一個單鏈表中。
若哈希表的長度是m,則可以將哈希表定義爲一個有m個頭指針組成的指針數組。散列地址爲i的記錄,均插到以指針數組第i個單元爲頭指針的單鏈表中。


性能指標

衡量查找效率的主要性能指標就是平均查找長度(ASL)。

ASL(succ)=(比較次數之和)/(關鍵字個數)

比較次數代表的是關鍵字放入散列地址中爲避免地址衝突需要跟當前散列地址中是否已經有值而進行判斷的次數。

ASL越小,性能越好。


哈希表

有了前面的基礎,我們來試着自己構建一個哈希表,並實現哈希表的插入,查找和刪除。


哈希表的創建

1.首先將表中各節點的關鍵字置空;
2.使用插入算法將給定的關鍵字序列一次插入哈希表中。


哈希表的插入

1.通過查找算法找到待插記錄在表中的位置;
2.若在表中找到待插記錄,則不必插入;若沒有找到,查找算法給出一個單元空閒的散列地址,並插入到該地址單元中。


哈希表的查找

1.根據待查找記錄的關鍵字和建表時的哈希函數計算散列地址;
2.若該地址單元爲空,則查找失敗;若不爲空,則將該單元中的關鍵字與待查記錄的關鍵字進行比較:
  如果相等,則查找成功;
  如果不等,則按建表時設定的處理衝突的方法找下一個地址。
3.重複上述步驟2,直至某個單元爲空,則查找失敗或者與待查記錄的關鍵字進行比較,相等則查找成功。


哈希表的刪除

基於開放地址法的哈希表不能實現真正的刪除,只能給被刪除節點設置刪除標誌,以免在刪除後找不到比它晚插入的節點且發生過沖突的節點,也就是說,如果執行真正的刪除操作,會中斷查找路徑,如果必須對哈希表做真正的刪除操作,最好採用鏈地址法處理衝突的哈希表。


哈希表的裝填因子

α = 哈希表中已存入的元素個數 / 哈希表的長度

α越小,衝突的可能性就越小,但空間利用率就越低;
α越大,衝突的可能性就越大,但空間利用率就越高。


哈希表的存儲效率爲何一般只有50%

根據上面的裝填因子我們可以得知,α越大,產生衝突的的機率也就越大,查找的次數就會變多,然後我們可以看一下查找的時間複雜度計算公式:

1/( 1 - n/m )

n/m就是上面所說的裝填因子,我們可以發現,當裝填因子大於1/2的時候,查找的時間複雜度就會大於二,所以我們一般會說哈希表的存儲效率只有50%。


哈希表的實現代碼(C語言)

github鏈接:哈希表

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