什麼是哈希表?

咱們在這篇文章將要學習最有用的數據結構之一—哈希表,哈希表的英文叫 Hash Table,也能夠稱爲散列表或者 Hash 表git

哈希表用的是數組支持按照下標隨機訪問數據的特性,因此哈希表其實就是數組的一種擴展,由數組演化而來。能夠說,若是沒有數組,就沒有散列表。github

哈希表存儲的是由鍵(key)和值(value)組成的數據。 例如,咱們將每一個人的性別做爲數據進行存儲,鍵爲人名,值爲對應的性別,其中 M 表示性別爲男,F 表示性別爲女。算法

爲何須要哈希表?

爲了和哈希表進行對比,咱們先將這些數據存儲在數組中。數組

此處準備了6個箱子(即長度爲6的數組)來存儲數據,假設咱們須要查詢 Ally 的性別,因爲不知道 Ally 的數據存儲在哪一個箱子裏,因此只能從頭開始查詢,這個操做便叫做線性查找。通常來講,咱們能夠把鍵當成數據的標識符,把值當成數據的內容。數據結構

從 0 號箱子開始查找,發現 0 號箱子中存儲的鍵是 Joe 而不是 Ally,所以接着查找 1 號箱子。函數

哦豁,1 號箱子中的也不是 Ally,沒辦法,只能接着往下找。性能

有點小糟糕,2 號、3 號箱子中的也都不是 Ally。學習

功夫不負有心人,當咱們查找到 4 號箱子的時候,發現其中數據的鍵爲 Ally,把鍵對應的值取出,咱們就知道 Ally 的性別爲女(F)。設計

經過上面的查找過程,咱們發現數據量越多,線性查找耗費的時間就越長。由此可知:因爲數據的查詢較爲耗時,因此此處並不適合使用數組來存儲數據。3d

但使用哈希表即可以解決這個問題,首先準備好數組,此次咱們用 5 個箱子的數組來存儲數據。

嘗試把 Joe 存進去,使用哈希函數(Hash)計算 Joe 的鍵,也就是字符串 Joe 的哈希值,好比獲得的結果爲4928。

將獲得的哈希值除以數組的長度 5,求得其他數,這樣的求餘運算叫做mod運算,此處mod運算的結果爲3。

所以,咱們將 Joe 的數據存進數組的 3 號箱子中,重複前面的操做,將其餘數據也存進數組中。

Sue 鍵的哈希值爲 7291, mod 5 的結果爲 1,將 Sue 的數據存進 1 號箱中。

Dan 鍵的哈希值爲 1539, mod 5 的結果爲 4,將 Dan 的數據存進 4 號箱中。

Nell 鍵的哈希值爲 6276, mod 5 的結果爲 1,本應將其存進數組的 1 號箱中,但此時 1 號箱中已經存儲了 Sue 的數據,這種存儲位置重複了的狀況便叫做衝突

遇到這種狀況,可以使用鏈表在已有數據的後面繼續存儲新的數據(鏈表法)。

Ally 鍵的哈希值爲 9143, mod 5 的結果爲 3,本應將其存儲在數組的 3 號箱中,但 3 號箱中已經有了 Joe 的數據,因此使用鏈表,在其後面存儲 Ally 的數據。

Bob 鍵的哈希值爲 5278, mod 5 的結果爲 3,本應將其存儲在數組的 3 號箱中,但 3 號箱中已經有了 Joe 和 Ally 的數據,因此使用鏈表,在 Ally 的後面繼續存儲 Bob 的數據。

像這樣存儲完全部數據,哈希表也就製做完成了。

接下來說解數據的查詢方法,假設咱們要查詢 Dan 的性別。

爲了知道 Dan 存儲在哪一個箱子裏,首先須要算出 Dan 鍵的哈希值,而後對其進行 mod 運算,最後獲得的結果爲 4,因而咱們知道了它存儲在 4 號箱中。

查看 4 號箱可知,其中的數據的鍵與 Dan 一致,因而取出對應的值,由此咱們便知道了 Dan 的性別爲男(M)。

那麼,想要查詢 Ally 的性別時該怎麼作呢?爲了找到它的存儲位置,先要算出 Ally 鍵的哈希值,再對其進行 mod 運算,最終獲得的結果爲 3。

然而 3 號箱中數據的鍵是 Joe 而不是 Ally,此時便須要對 Joe 所在的鏈表進行線性查找。

因而咱們找到了鍵爲 Ally 的數據,取出其對應的值,便知道了 Ally 的性別爲女(F)。

哈希衝突

在哈希表中,咱們能夠利用哈希函數快速訪問到數組中的目標數據。若是發生哈希衝突,就使用鏈表進行存儲,這樣一來,無論數據量爲多少,咱們都可以靈活應對。

若是數組的空間過小,使用哈希表的時候就容易發生衝突,線性查找的使用頻率也會更高;反過來,若是數組的空間太大,就會出現不少空箱子,形成內存的浪費。所以,給數組設定合適的空間很是重要。

在存儲數據的過程當中,若是發生衝突,能夠利用鏈表在已有數據的後面插入新數據來解決衝突,這種方法被稱爲鏈表法,也被稱爲鏈地址法

其中在 Java 集合類的 HashMap 中解決衝突的方法就是採用的鏈表法,建議閱讀 HashMap 源碼。

除了鏈地址法之外,還有幾種解決衝突的方法。其中,應用較爲普遍的是開放地址法,或稱爲開放尋址法。這種方法是指當衝突發生時,馬上計算出一個候補地址(數組上的位置)並將數據存進去。若是仍然有衝突,便繼續計算下一個候補地址,直到有空地址爲止,能夠經過屢次使用哈希函數線性探測法等方法計算候補地址。

在 Java 中,ThreadLocal 所使用的就是開放地址法

哈希函數設計的好壞決定了哈希衝突的機率,也就決定哈希表的性能。

總結

這篇文章主要講了一些比較基礎的哈希表知識,包括哈希表的由來、哈希衝突的解決方法。

哈希表也叫散列表,來源於數組,它藉助哈希函數對數組這種數據結構進行擴展,利用的是數組支持按照下標隨機訪問元素的特性,是存儲 Key-Value 映射的集合。

哈希表兩個核心問題是哈希函數設計和哈希衝突解決。對於某一個 Key,哈希表能夠在接近 O(1) 的時間內進行讀寫操做。哈希表經過哈希函數實現 Key 和數組下標的轉換,經過開放尋址法和鏈表法來解決哈希衝突。哈希函數設計的好壞決定了哈希衝突的機率,也就決定哈希表的性能。

有興趣的能夠在 JDK 中閱讀 HashMap 的源碼,在 JDK 8 和以前的版本的實現還有許多很少,好比在 JDK 8 中,引入紅黑樹,當鏈表長度太長(默認超過 8)時,鏈表就轉換爲紅黑樹,就能夠利用紅黑樹快速增刪改查的特色,提升 HashMap 的性能。

參考

《個人第一本算法書》

https://github.com/wupeixuan/JDKSourceCode1.8

相關文章
相關標籤/搜索