1. 程式人生 > >資料結構:雜湊表以及雜湊衝突的解決方案

資料結構:雜湊表以及雜湊衝突的解決方案

前言

基於先前的學習計劃,最近打算深入學習Java的集合類,首先要研究的就是HashMap,在學習HashMap前,我花了幾天時間溫習了一下類中用到的資料結構 (雜湊表,二叉樹),並決定把所學的知識記錄寫成文章,本文講述的就是關於雜湊表的知識。

什麼是雜湊表

在之前的部落格文章裡,我們簡單介紹了資料結構的幾種分類,其中就包括雜湊表,也稱散列表,從根本上來說,一個雜湊表包含一個數組,通過特殊的關鍵碼(也就是key)來訪問陣列中的元素。雜湊表的主要思想是通過一個雜湊函式, 把關鍵碼對映的位置去尋找存放值的地方 ,讀取的時候也是直接通過關鍵碼來找到位置並存進去。

最直接的例子就是字典,例如下面的字典圖,如果我們要找 “啊” 這個字,只要根據拼音 “a” 去查詢拼音索引,查詢 “a” 在字典中的位置 “啊”,這個過程就是雜湊函式的作用,用公式來表達就是:f(key),而這樣的函式所建立的表就是雜湊表。比起陣列和連結串列查詢元素時需要遍歷整個集合的情況來說,雜湊表明顯方便和效率的多。

常見的雜湊演算法

雜湊表的組成取決於雜湊演算法,也就是雜湊函式的構成,下面列舉幾種常見的雜湊演算法。

1) 直接定址法

  • 取關鍵字或關鍵字的某個線性函式值為雜湊地址。
  • 即 f(key) = key 或 f(key) = a*key + b,其中a和b為常數。

2) 除留餘數法

  • 取關鍵字被某個不大於散列表長度 m 的數 p 求餘,得到的作為雜湊地址。
  • 即 f(key) = key % p, p < m。這是最為常見的一種雜湊演算法。

3) 數字分析法

  • 當關鍵字的位數大於地址的位數,對關鍵字的各位分佈進行分析,選出分佈均勻的任意幾位作為雜湊地址。
  • 僅適用於所有關鍵字都已知的情況下,根據實際應用確定要選取的部分,儘量避免發生衝突。

4) 平方取中法

  • 先計算出關鍵字值的平方,然後取平方值中間幾位作為雜湊地址。
  • 隨機分佈的關鍵字,得到的雜湊地址也是隨機分佈的。

5) 隨機數法

  • 選擇一個隨機函式,把關鍵字的隨機函式值作為它的雜湊值。
  • 通常當關鍵字的長度不等時用這種方法。

雜湊衝突

雜湊表因為其本身的結構使得查詢對應的值變得方便快捷,但也帶來了一些問題,以上面的字典圖為例,key中的一個拼音對應一個字,那如果字典中有兩個字的拼音相同呢?例如,我們要查詢 “按” 這個字,根據字母拼音就會跳到 “安” 的位置,這就是典型的雜湊衝突問題。這個時候用公式表達就是:

key1 ≠  key2  , f(key1) = f(key2)

一般來說,雜湊衝突是無法避免的,如果要完全避免的話,那麼就只能一個字典對應一個值的地址,也就是一個字就有一個索引 (就是兩個索引),這樣一來,空間就會增大,甚至記憶體溢位。

雜湊衝突的解決辦法

常見的雜湊衝突解決辦法有兩種,開放地址法和鏈地址法。

一、開放地址法

開發地址法的做法是,當衝突發生時,使用某種探測演算法在散列表中尋找下一個空的雜湊地址,只要散列表足夠大,空的雜湊地址總能找到。按照探測序列的方法,一般將開放地址法區分為線性探查法、二次探查法、雙重雜湊法等。

這裡為了更好的展示三種方法的效果,我們用以一個模為8的雜湊表為例,採用除留餘數法,往表中插入三個關鍵字分別為26,35,36的記錄,分別除8取模後,在表中的位置如下:

這個時候插入42,那麼正常應該在地址為2的位置裡,但因為關鍵字30已經佔據了位置,所以就需要解決這個地址衝突的情況,接下來就介紹三種探測方法的原理,並展示效果圖。

1) 線性探查法:

fi=(f(key)+i) % m ,0 ≤ i ≤ m-1

探查時從地址 d 開始,首先探查 T[d],然後依次探查 T[d+1],…,直到 T[m-1],此後又迴圈到 T[0],T[1],…,直到探查到有空餘的地址或者到 T[d-1]為止。

插入42時,探查到地址2的位置已經被佔據,接著下一個地址3,地址4,直到空位置的地址5,所以39應放入地址為5的位置。

缺點:需要不斷處理衝突,無論是存入還是査找效率都會大大降低。

2) 二次探查法

fi=(f(key)+di) % m,0 ≤ i ≤ m-1

探查時從地址 d 開始,首先探查 T[d],然後依次探查 T[d+di],di 為增量序列1^2,-1^2,2^2,-2^2,……,q^2,-q^2 且q≤1/2 (m-1) ,直到探查到 有空餘地址或者到 T[d-1]為止。

缺點:無法探查到整個雜湊空間。

所以插入42時,探查到地址2被佔據,就會探查T[2+1^2]也就是地址3的位置,被佔據後接著探查到地址7,然後插入。

3) 雙雜湊函式探測法

fi=(f(key)+i*g(key)) % m (i=1,2,……,m-1)

其中,f(key) 和 g(key) 是兩個不同的雜湊函式,m為雜湊表的長度

步驟:

雙雜湊函式探測法,先用第一個函式 f(key) 對關鍵碼計算雜湊地址,一旦產生地址衝突,再用第二個函式 g(key) 確定移動的步長因子,最後通過步長因子序列由探測函式尋找空的雜湊地址。

比如,f(key)=a 時產生地址衝突,就計算g(key)=b,則探測的地址序列為 f1=(a+b) mod m,f2=(a+2b) mod m,……,fm-1=(a+(m-1)b) % m,假設 b 為 3,那麼關鍵字42應放在 “5” 的位置。

雜湊表效能

​ 雜湊表的特性決定了其高效的效能,大多數情況下查詢或者插入元素的時間複雜度可以達到O(1), 時間主要花在計算hash值上, 然而也有一些極端的情況,最壞的就是hash值全都對映在同一個地址上,這樣雜湊表就會退化成連結串列,例如下面的圖片: 當hash表變成圖2的情況時,時間複雜度會變為O(n),效率瞬間低下,所以,設計一個好的雜湊表尤其重要,如HashMap在jdk1.8後引入的紅黑樹結構就很好的解決了這種情況。