Java集合原始碼分析之基礎(二):雜湊表

大大紙飛機發表於2018-08-08

無論是陣列還是連結串列,其對資料的查詢表現都比較無力,要想知道一個元素是否在陣列或連結串列中,只能從前向後挨個對比。出現這個問題的根源在於,我們沒有辦法直接根據一個元素找到它儲存的位置,那有沒有辦法消除這個對比的過程呢?

雜湊表就是解決查詢問題的一種方案。在後續將會分析的二叉排序樹中,還會將資料排序以進行二分查詢,將時間複雜度從O(n)降低到O(lg n)

雜湊表與Hash函式

通俗來講,雜湊表就是通過關鍵字來獲取資料的一種資料結構,它通過把關鍵字對映為表中的位置來獲取元素,這種對映主要是使用Hash函式。

Hash函式,實際上是建立起key值與int值對映關係的函式。這就好比我們每個人都有一個身份證號一樣,無論是男是女,出生在何處,都可以通過身份證號來分辨,這就是把人的資訊對映成一串數字的典型做法。Hash函式和此類似,不過是把任意的Java物件,對映成一個int數值,供雜湊表使用。

而雜湊表,就是一個陣列,只是其元素不是按照陣列的規則排列的。任何一個元素要放進雜湊表中,都必須先通過Hash函式獲取到一個int數值,這個數值經過處理後將作為它的存放位置,然後這個元素才能放進雜湊表中。

可以發現,陣列與雜湊表的操作不同之處主要在於,前者是直接插入,後者需要通過Hash函式計算後再插入。可以通過下圖對比來理解:

陣列的插入

雜湊表的插入

雜湊表完全繼承了陣列的優點,又顯著的提高了查詢的速度,通過Hash函式使得查詢速度達到了O(1)。既然有了雜湊表,它這麼優秀,為何還需要陣列的存在呢?那是因為Hash表是有缺陷的,這個缺陷就是雜湊碰撞

雜湊碰撞

Hash函式所做的事,就是無論什麼物件,都根據一個規則對映為一個int值。被轉換的物件有無數種可能,但是int的值是有限的,它只有2^32^個,這樣一來,必然會有不同的物件,對映得到相同的int值,這就是所謂的雜湊碰撞。發生碰撞之後,就要把不同的元素插入到相同的位置,這時候單純的使用一維陣列已經無法滿足需求了。

解決雜湊碰撞的方法

要解決雜湊碰撞,我們可以想到多種解決方案。例如使用二維陣列,將碰撞的元素按順序儲存起來,類似下圖:

二維陣列儲存

這樣的方式有一個很大的詬病,因為陣列大小是固定的,所以第二維的陣列長度都是一樣的,但是雜湊碰撞一定是比較少發生的情況,也就是我們宣告瞭一個很大的陣列,但是其中大部分都是閒置的,這就浪費了大量的記憶體。

還有一些方案是考慮了雜湊表的雜湊化,將元素插入到空閒的位置去。因為雜湊表基本不會像陣列一樣每個位置都有元素,這樣就可以將碰撞的元素插入到這些空閒的位置中區,這種方案稱為定址法。但是這個方法在擴充套件性上表現不佳,我們這裡就不再浪費篇幅來解釋它了。

目前比較通用的方法,就是使用陣列+連結串列組合的方式。當出現雜湊碰撞時,在該位置的資料就通過連結串列的方式連結起來,如下圖所示:

雜湊表的結構示意圖

這是當前比較理想的方法,既繼承了陣列的優點,又在碰撞時繼承了連結串列的優點,這也是雜湊表強大的地方之一。

在JDK1.7及之前的版本中,HashMap的儲存結構和上圖是一致的,在JDK1.8之後還加入了紅黑樹以進一步優化,在後續文章中我們會對其進行詳盡的分析。

雜湊表的優缺點

雜湊表是一種優化儲存的思想,具體儲存元素的依然是其他的資料結構。設計良好的雜湊表,能同時兼備陣列和連結串列的優點,它能在插入和查詢時都具備良好的效能。然而設計不好的雜湊表,有可能會出現較多的雜湊碰撞,導致連結串列過長,從而雜湊表會更像一個連結串列。還有當資料量很大時,為防止連結串列過長,就需要對陣列進行擴容,這時就涉及到了陣列的拷貝,其對效能的影響也很嚴重,所以需要提前對可能的情況有良好的預測,才能真正發揮雜湊表的優勢。

上一篇:Java集合原始碼分析之基礎(一):陣列與連結串列

下一篇:Java集合原始碼分析之基礎(三):樹與二叉樹

本文到此就結束了,如果您喜歡我的文章,可以關注我的微信公眾號:大大紙飛機

或者掃描下方二維碼直接新增:

公眾號

您也可以關注我的github:https://github.com/LtLei/articles

程式設計之路,道阻且長。唯,路漫漫其修遠兮,吾將上下而求索。

相關文章