前言
基於先前的學習計划,最近打算深入學習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 為增量序列12,-12,22,-22,……,q2,-q2 且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后引入的紅黑樹結構就很好的解決了這種情況。
