HashMap 設計原理與實現細節解析

2021-10-23 04:22:42 字數 3962 閱讀 1966

知其然也要知其所以然,很多人講 hashmap 都是看下原始碼,分析下,然後知道了什麼拉鍊法,桶,紅黑樹,看了一圈,好像懂了,又好像沒懂。本著死苛的精神,我來嘗試談下我對 hashmap 理解,個人只見,有錯誤之處,歡迎批評指出。

為什麼要設計 hashmap?

map 我們都很好理解,就是 key,value 的容器。用來儲存和獲取以鍵值對存在的資料結構。比如統計每個人有多少 money:

張三100

李四90

王二80

按照正常的簡單需求,我們一般對需要資料進行增刪改查,無論是增、刪、改、查,在計算機中對資料訪問最重要的是更快的查到資料正確的儲存位置。怎樣來設計這種key、value 資料結構的儲存,使得增刪改查都比較高效呢?下面我們就來整理思路,並嘗試理解 hashmap 的設計思路及實現細節。

首先,我們針對上面的資料實體抽象出乙個實體類

class entry
現在我們用最簡單的思路順序儲存或者鏈式儲存,把這些資料存起來,如下圖:

按照上面的儲存方式,給定 key 值,我們想要查詢乙個 entry,需要對比遍歷看 key 值是否相等,才能查到。顯然 map 用這兩種方式實現都是很低效的。

理想狀態下,我們想要實現的是給定 key 值,就能直接得到儲存位置,這樣查詢效率就很高。要想實現這樣的目的,很顯然,key 值要和具體的位址產生對應關係。而這種對應關係,就是 hash 函式。那麼怎麼來構造這種對應關係呢?

因為鏈式儲存即使知道儲存的相對位置也許要多次查詢,所以這種實現必然是和順序儲存相關的。

所以,我們可以這樣設計對應關係:

首先任意長度的字串行都能由固定的 hash 演算法生成一串 hashcode ,所以每個 key 都有自己的 hashcode。我們把這個 hashcode 看做乙個二進位制數。當然這些二進位制數很大,要想使他們對映到乙個連續的較小的順序儲存的下標,最簡單的辦法就是對這些數求餘數啦。

假設:張三的 hashcode = 101000111000(二進位制)

李四的 hashcode = 100000001101(二進位制)

王二的 hashcode = 100000111110(二進位制)

為了方便求餘計算,hashmap 容量為 100(二進位制)

得出:張三:100000001101%100 = 0

李四:100000001101%100 = 1

王二:100000111110%100 = 2

所以hashmap 中這三個 entry 的儲存圖如下:

這樣的資料結構就滿足了高效率的要求。根據 key 的 hashcode 進行求餘就可以找到對應 hashmap 中的位置進行增刪改查了。

現在還存在這樣乙個問題,如果劉五有 70 塊錢,這個資料我們也想存進 hashmap:

劉五的 hashcode = 111110111101 對 100 求余得 1,可是1 的位置已經存入了李四的資料,這時候該怎麼辦呢?

這種情況,其實就是 hash 碰撞,又叫 hash 衝突。我們把求餘看做是 hash 函式的計算公式,只要存入的資料多於 hashmap 的容量,就必然有重複,必然有衝突。其實就算乙個很大的 hashmap,只存入兩個entry,也有衝突的可能(hashcode 餘數相同)。

針對這種 hash 碰撞,我們該如何處理呢?其實也很好解決,可以用下圖的方式解決:

這就是拉鍊法,先根據 key 的 hashcode 進行求餘,得到陣列下標,然後再比較 key 值,確定要增刪改查的 entry,這也就意味著entry 的資料結構需要再增加乙個 next 指標:

class entry
到這裡 hashmap 我們已經設計的差不多了,其實原始碼裡也就這點東西。下面跟一下原始碼,對一些概念做下補充。

首先來看 hashmap 的建構函式:

public hashmap(int initialcapacity, float loadfactor)
initialcapacity 初始容量

loadfactor 載入因子

hashmap 建立的時候可以設定乙個初始容量(沒有設定則預設為 16),上面我們已經講了,這個容量是用來求餘計算桶下標的。如果容量過小,而儲存的資料過多,就會出現大量的碰撞:

如上圖:桶 1 的位置已經有 5 個 entry 了。如果容量小,而要存的資料多,可能會有更長的鍊錶,這時如果要操作鍊錶尾端的 ccc就會大大降低 hashmap 的效率。所以必需對這種情況進行處理,處理方法:

當 hashmap 儲存的 entry 多於某個臨界點時,就要對 hashmap 進行擴容。這個初始預設臨界點就是

initialcapacity * loadfactor

loadfactor 預設取值是 0.75,下面是 hashmap 判斷擴容的**:

final v putval(int hash, k key, v value, boolean onlyifabsent,

boolean evict)

++modcount;

if (++size > threshold)

//當容量大於臨界值,進行擴容

resize();

afternodeinsertion(evict);

return null;

}final node resize()

else if ((newcap = oldcap << 1) < maximum_capacity &&

oldcap >= default_initial_capacity)

//臨界值變為原來的兩倍,容量也變為原來的兩倍,臨界值依然是 容量*載入因子

newthr = oldthr << 1; // double threshold

}else if (oldthr > 0) // initial capacity was placed in threshold

newcap = oldthr;

else

if (newthr == 0)

threshold = newthr;

@suppresswarnings()

node newtab = (node)new node[newcap];

table = newtab;

if (oldtab != null)

return newtab;

}

上面**加注釋已經很清楚了,擴容臨界點就是:當前容量 *載入因子。

容量選擇:hashmap 容量選擇都是 2 的 n 次方。

因為每次擴容,都要重新計算餘數,再一次整理 hashmap 裡的桶和鍊錶,會有較大的效能開銷。選2 的 n 次方,是為了進行位運算,提高效率。

對 2 的 n 次方求餘可以用 & 進行位運算,這個很簡單不再展開。

關於紅黑樹: 1.8 引入紅黑樹,不過是對上面拉鍊法的補充。

如果某個桶上的鍊錶過長,也就是好多個 key  的 hashcode 求餘得到的位置指向同乙個桶,會導致針對這個桶鍊錶的操作效率會大大降低,為了改善這種情況引入了紅黑樹,當鍊表節點大於 7 個時,把鍊錶結構轉化為紅黑樹結構,來降低查詢的開銷。紅黑樹只是平衡二叉樹的一種實現,其目的就是減少查詢層級,優化極端情況下的查詢速度。這裡理解其優化思想即可,不再深究。

最後,hashmap 圖(懶得畫了,網上找的,侵刪)

HashMap實現原理

hashmap 的get 方法 呼叫get方法返回entry public v get object key getentry方法 final entrygetentry object key 對key int hash key null 0 hash key for entrye table in...

HashMap實現原理

資料結構中有陣列和鍊錶來實現對資料的儲存,但這兩者基本上是兩個極端。陣列儲存區間是連續的,占用記憶體嚴重,故空間複雜的很大。但陣列的二分查詢時間複雜度小,為o 1 陣列的特點是 定址容易,插入和刪除困難 鍊錶儲存區間離散,占用記憶體比較寬鬆,故空間複雜度很小,但時間複雜度很大,達o n 鍊錶的特點是...

HashMap實現原理

public v put k key,v value 如果i索引處的entry為null,表明此處還沒有entry。modcount 將key value新增到i索引處。addentry hash,key,value,i return null 從上面的源 中可以看出 當我們往hashmap中put...