關於HashMap的一些思考
阿新 • • 發佈:2021-03-30
一、HashMap的負載因子的作用
當 HashMap 中的元素個數(包含連結串列、紅黑樹上的元素)達到陣列長度的0.75倍的時候,開始擴容。二、HashMap的負載因子為什麼是0.75
主要是為了提高空間利用率和減少查詢成本(也可以說是儘可能減少hash衝突)。三、為什麼槽位數必須使用2^n
如果想讓 Hash 結果分佈更加均勻,首先想到的就是使用取餘(%)操作。重點來了:“取餘(%)操作中如果除數是2的冪次則等價於與其除數減一的與(&)操作(也就是說 hash % length == hash & (length - 1) 的前提是 length 是 2 的 n 次方)。” 並且採用二進位制位操作 &,相對於 % 能夠提高運算效率,這就解釋了 HashMap 的長度為什麼是 2 的冪次方。四、解決Hash衝突的方法
1、開放地址法
公式:fi(key) = (f(key)+di) MOD m (di=0,1,2,3,......,m-1) key:待放入陣列(hash表)的元素;m:陣列長度 當衝突發生時,使用某種探測技術在散列表中形成一個探測序列。沿此序列逐個單元地查詢,直到找到給定的關鍵字,或者碰到一個開放的地址(即該地址單元為空)為止(若要插入,在探查到開放的地址,則可將待插入的新結點存人該地址單元)。查詢時探測到開放的地址則表明表中無待查的關鍵字,即查詢失敗。(1)線性探測法
思想是:通過公式計算出元素在陣列中的下標,如果下標上沒有元素,直接放進去;如果下標中有元素,則公式中的 di 依次 +1 重新計算,直到查詢到沒有元素的下標。不然陣列就滿了,需要擴容。(2)二次探測法
(3)偽隨機數探測再雜湊
思想是:di 的值是通過隨機函式得到的。如果隨機函式的種子相同,那麼得出來的 di 也相同,查詢就ok了。 總之,開放定址法只要在散列表未填滿時,總是能找到不發生衝突的地址,是我們常用的解決衝突的辦法。2、拉鍊法
五、為什麼連結串列長度達到 8 的時候就要轉為紅黑樹了?
當使用 0.75 作為負載因子時,連結串列中的長度達到 8 幾乎是不可能的,均衡策略吧。 引用 HashMap 原始碼中的註釋:* 0: 0.60653066 * 1: 0.30326533 * 2: 0.07581633 * 3: 0.01263606 * 4: 0.00157952 * 5: 0.00015795 * 6: 0.00001316 * 7: 0.00000094 * 8: 0.00000006 * more: less than 1 in ten million
六、HashMap擴容時元素的位置發生了什麼變化?
分為三種情況:- 對於陣列上的元素:直接使用已經計算出來的hash值重新計算新下標放入新陣列。
- 對於連結串列:將一條連結串列拆分為兩條,hash值大於陣列長度的新連結串列放在新陣列,小於的就放在原陣列。
- 對於紅黑樹:將數拆為兩條連結串列,hash值大於陣列長度的新連結串列放在新陣列,小於的就放在原陣列,最後,重新判斷兩條連結串列是否需要轉為紅黑樹。
do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null);例如:oldCap 是 16,那麼擴容之後的新陣列長度就是 32,連結串列上的元素分別是 7,23,39。(整數的hash值就是本身)
7 :0000 0111 & 16:0001 0000 --------------- = :0000 0000 # 0,仍舊在原位 17:0001 0001 & 16:0001 0000 --------------- = :0001 0000 # 非0,需要放在 [17, 32) 之間 23:0001 0111 & 16:0001 0000 --------------- = :0001 0000 # 非0,需要放在 [17, 32) 之間 39:0010 0111 & 16:0001 0000 --------------- = :0000 0000 # 0,仍舊在原位,因為它的的值大於陣列的長度