詳解HashMap中的Hash算法(擾動函數)

面試中經常會問HashMap的源碼,因爲HashMap不僅是日常開發中最常用到的類,還因爲裏面還包括了很多巧妙的算法。

HashMap裏對Key取Hash和通過Hash找到在數組中的位置需要調用下面兩段代碼:

// 以下來源JDK8源碼:

// 找到元素在數組中的位置,n爲數組長度。
i = (n - 1) & hash

// 計算Key的Hash值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

爲什麼HashMap數組的長度要是2的整數冪?

我們希望理想的情況是:任意一個Key落在數組中的位置是足夠散列的,這樣可以減少Hash碰撞的概率。

假設計算出的Hash值是足夠散列的,由於Hash值是一個int類型的值,大部分情況下HashMap數組是不會那麼長的。所以在有限的數組長度內,當然是取Hash值的低幾位算是比較理想的散列方式。

正因爲此,而任何2的整數冪,減一得到的二進制位全部是一。如:16-1=15,二進制表示爲:1111;32-1=31,二進制表示爲:11111。所以讓Hash值與(&)上n-1後得到的就是低位Hash值,如:

    00100100 10100101 11000100 00100101    // Hash值
&   00000000 00000000 00000000 00001111    // 16 - 1 = 15
----------------------------------
    00000000 00000000 00000000 00000101    // 高位全部歸零,只保留末四位。

Hash算法(擾動函數)

接着上面的理解,所以我們需要一個Hash函數得到足夠散列的Hash值。

而任何一個Object類型的hashCode方法得到的Hash值是一個int型,Java中int型是4*8=32位的。顯然很少有HashMap的數組有40億這麼長。如果只是取低幾位的Hash值的話,那麼那些低位相同,高位不同的Hash值就碰撞了,如:

// Hash碰撞示例:
H1: 00000000 00000000 00000000 00000101 & 1111 = 0101
H2: 00000000 11111111 00000000 00000101 & 1111 = 0101

爲了解決這類問題,HashMap想了一種辦法(擾動):將Hash值的高16位右移並與原Hash值取異或運算(^),混合高16位和低16位的值,得到一個更加散列的低16位的Hash值。如:

00000000 00000000 00000000 00000101 // H1
00000000 00000000 00000000 00000000 // H1 >>> 16
00000000 00000000 00000000 00000101 // hash = H1 ^ (H1 >>> 16) = 5

00000000 11111111 00000000 00000101 // H2
00000000 00000000 00000000 11111111 // H2 >>> 16
00000000 00000000 00000000 11111010 // hash = H2 ^ (H2 >>> 16) = 250

最終:

// 沒有Hash碰撞
index1 = (n - 1) & H1 = (16 - 1) & 5 = 5
index2 = (n - 1) & H2 = (16 - 1) & 250 = 10

再加上在程序中位運算是很快的,所以這算是一種非常巧妙並且高效的Hash函數。

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