白話文講HashMap

在這片文章開始之前,我先拋出幾個問題,讀者可以先回憶或者思考一下,然後再繼續往下看,看與讀者之前的認識是否有衝突
1、HashMap底層是一種什麼樣的結構?
2、一個對象最後是如何確定到一個Hash桶的(如何確定數組中的一個位置)?
3、發生Hash衝突瞭如何解決?
4、爲什麼HashMap需要擴容?
5、爲什麼HashMap容量是2的冪次方
6、引入紅黑樹解決了什麼樣的問題?
7、什麼時候擴容?是否不達到閾值就一定不擴容呢?

由於都是我自己的觀點,所以希望讀者能給我留言,說說讀後感,交流一下,若有錯誤歡迎拍磚:

個人覺得HashMap這個東東可以挖掘出很多很多基礎知識,當然這些東西都是我有意挖掘後整理的。

首先第一個問題:HashMap底層是一種什麼樣的結構?
答案大家肯定都知道:數組+鏈表,如下圖所示:

這裏寫圖片描述

爲什麼會是這樣一種結構呢?
現在來說第二個問題,一個對象是如何定位到一個數組中的?
HashMap是一種key,value的鍵值對存儲形式,當添加一個對象到HashMap中的時候,要進過以下過程:

1、獲得該對象的hashcode

現在咱們假設有3個對象需要加入到HashMap中,這三個對象的hashcode分別是:
A:0100 0101 0110 1111 0101 0111 0110 0111
B:0100 0111 1111 0000 0010 1101 0011 1111
C: 0100 0111 1111 0000 0010 1101 0011 0001

2、經過一次擾亂函數(擾亂函數其實就是把對象的hashcode高16與低16位作與運算)重新獲得32位的二進制數,這裏博主主要想描述的不在於此,所以就假設經過擾亂函數之後,A、B、C三個對象的hashcode都不變 仍然是:

A:0100 0101 0110 1111 0101 0111 0110 0111
B:0100 0111 1111 0000 0010 1101 0011 1111
C: 0100 0111 1111 0000 0010 1101 0011 0001

3、用HashMap的size-1與第二步得到的hashcode作&(與運算),得到的數字是幾,就把這個對象放在數組的第幾個位置上,舉例說明:

假設HashMap的size爲8,則size二進制爲:
0000 0000 0000 0000 0000 0000 0000 1000
那size-1之後的二進制數爲:
0000 0000 0000 0000 0000 0000 0000 0111
這個時候size-1分別與A,B,C做與運算
例如與A作與運算:
0100 0101 0110 1111 0101 0111 0110 0111
0000 0000 0000 0000 0000 0000 0000 0111 &
————————————————————————
0000 0000 0000 0000 0000 0000 0000 0111

B、C與運算結果分別爲:
0000 0000 0000 0000 0000 0000 0000 0111
0000 0000 0000 0000 0000 0000 0000 0001

上面說了,作完與運算之後數字就是放在數組中的位置,位運算之後結果轉爲10進制分別是 A:7,B:7,C:1
這個時候A和B由於計算結果相等(即發生了hash衝突)所以需要放在數組中的同一個位置,數組的一個位置只能放一個元素嘛,所以有了鏈表,所以HashMap纔有了數組+鏈表的結構。

現在讓我們再看看第4個問題:爲什麼HashMap需要擴容?
這個我覺得其實很有意思,大家學習容器的過程應該一般都是先學了ArrayList,LinkedList,然後學了HashSet,TreeSet,最後學了HashMap,不知道大家是不是這樣,至少我是這樣一個過程,導致了我很長一段時間內對HashMap的“擴容”產生了誤解,我們知道ArrayList爲什麼需要擴容?就是因爲ArrayList所維護的數組滿了,放不下元素了,才擴容。

那HashMap擴容是不是也因爲是放不下元素呢?
答案是:HashMap的擴容其實跟放不放得下完全沒關係,大家想一下HashMap這種結構是不是永遠不會滿?如果HashMap中的數組滿了,有新元素添加進來,也一定會插入到數組中的某條鏈表上,對吧?

那HashMap既然永遠不會滿,那爲什麼要擴容?擴容解決了什麼樣的問題呢?
先讓我們想想HashMap這種數據結構設計的初衷:理想情況下,可以通過key的hash值在時間複雜度爲O(1)拿到對應的Value對吧?那現在假設HashMap中的數組上每一個位置都有一條特別特別長的鏈表,那當我們通過key去定位到數組的某個位置了之後,是不是還需要遍歷這個位置上的鏈表最終才能拿到那個元素?那時間複雜度就不是O(1)而近似爲O(元素個數/數組長度)。

達不到理想狀態怎麼辦呢?我們總得去解決這個問題吧?
於是有了兩種解決方案:
1、在適當的時候,將數組擴容,你想想一下如果數組無限長是不是不同對象定位到數組中的同一個位置的概率就非常小了(數組無限長的時候只有在不同對象的Hashcode相等的時候纔會出現)?
所以纔有了什麼負載因子,擴容機制這些東東,這些東東就解決一件事情,降低發生hash衝突的概率,降低發生hash衝突的概率就是跟我開始說的HashMap設計的初衷相合,即:發生hash衝突的次數越小,越接近理想情況,在時間複雜度爲O(1)的情況下通過key去get到對應的value。

2、第二種解決方案比較有意思,即當鏈表長度達到8的時候,會執行樹化函數,其實就是把鏈表轉成一顆紅黑樹(個人覺得轉紅黑樹是一種治標不治本的無奈之舉),由於本文討論的是HashMap,這裏作者不需要專門去看紅黑樹,讀者可以簡單的理解紅黑樹是一顆近似平衡的二叉排序樹,如果你也不知道什麼是二叉排序樹也不要緊,你就先記住在紅黑樹上找一個對象比在鏈表上找一個對象要快就行了,說白了鏈表轉爲紅黑樹就是爲了提高HashMap中查找元素的速率。

文章一開始提到的問題,這裏已經解決了一大半,既然說到了擴容,我先再好好說說擴容的時候發生了什麼,之間原數組中的元素是放在新數組的同樣索引的位置嗎?
這個時候會重新定位數組中的每個對象的位置,讓我們重新回顧一下A,B之前經過擾亂函數之後的值:

A:0100 0101 0110 1111 0101 0111 0110 0111
B:0100 0111 1111 0000 0010 1101 0011 1111
在數組長度爲8的時候,A,B與(數組長度-1)作與運算之後都爲7,大家應該都記得吧,A,B之前是在同一條鏈表上,擴容之後他們將重新定位到新的數組上,大家可以先根據前面知識,想一想A,B在新的數組上會不會發生hash衝突,即仍然定位到新數組上的同一位置呢?

答案是:不會。 why?

數組擴容後,新數組長度爲16
二進制位:
0000 0000 0000 0000 0000 0000 0001 0000

新數組長度-1的二進制是:
0000 0000 0000 0000 0000 0000 0000 1111

然後用A、B的最終hash值與上述二進制作與運算,來看結果:

A:
0000 0000 0000 0000 0000 0000 0000 1111
0100 0101 0110 1111 0101 0111 0110 0111 &
————————————————————————
0000 0000 0000 0000 0000 0000 0000 0111

-

B:
0000 0000 0000 0000 0000 0000 0000 1111
0100 0111 1111 0000 0010 1101 0011 1111
————————————————————————
0000 0000 0000 0000 0000 0000 0000 1111

發現沒有?A是0111 而B是1111,A是7,B是15,換句話說A定位在新數組的第7位置上,B定位在新數組的第15位置上,分開了,很神奇有木有?

結論來了:在擴容的時候,原數組的鏈表會分成兩條鏈表,一條鏈表在新數組的原索引位置,一條鏈表在新數組的新索引位置,而且這兩條鏈表在概率上是近乎相等的,可能有點繞,讀者就看上面A、B的情況,A之前在舊數組的第7個位置,在擴容後,A仍在在新數組的第7位,而B之前在舊數組的第7位,擴容後在新數組的第15位。有點扯,擴容就可以把鏈表變短也~擴容一次鏈表幾乎變短一半,很神奇有沒有~~

還沒寫完,這是我來百度的第二天,現在時間是晚上10:08分,周圍還很熱鬧,我沒事幹寫了這篇文章,有點點累準備回家睡覺了~~ 之後接着更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章