知其然也要知其所以然,很多人講 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...