天天看點

由HashMap雜湊演算法引出的求餘%和與運算&轉換問題

1、引出問題

  在前面講解

HashMap

 的源碼實作時,有如下幾點:

  ①、初始容量為 1<<4,也就是24 = 16

  

由HashMap雜湊演算法引出的求餘%和與運算&amp;轉換問題

  ②、負載因子是0.75,當存入HashMap的元素占比超過整個容量的75%時,進行擴容,而且在不超過int類型的範圍時,進行2次幂的擴充(指長度擴為原來2倍)

由HashMap雜湊演算法引出的求餘%和與運算&amp;轉換問題

  擴大一倍

由HashMap雜湊演算法引出的求餘%和與運算&amp;轉換問題

  ③、新添加一個元素時,計算這個元素在HashMap中的位置,也就是本篇文章的主角 哈希運算。分為三步:

  第一步:取 hashCode 值: key.hashCode()

  第二步:高位參與運算:h>>>16

  第三步:取模運算:(n-1) & hash

1     static final int hash(Object key) {
2         int h;
3         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4     }
5 
6     tab[i = (n - 1) & hash];      

  ps:第 6 行代碼是我自己加的。

  我們知道一個好的 雜湊演算法能夠使得元素分布的更加均勻,進而減少哈希沖突。HashMap 在這塊的處理就很巧妙:

  首先第一步取得 hashCode,該方法是一個用native修飾的本地方法,傳回的是一個 int 類型的值(根據記憶體位址換算出來的一個值),通常我們都會重寫該方法。

  第二步将取得的哈希值無符号右移16位,高位補0。并與前面第一步獲得的hash碼進行按位異或^ 運算。這是為了當length比較小的時候,也能保證考慮到高低Bit位都參與到Hash的計算中,同時不會有太大的開銷。

由HashMap雜湊演算法引出的求餘%和與運算&amp;轉換問題

  本文的重點是第三步,将經過前面兩步擷取的 hash 值,與HashMap的集合長度減 1 進行按位與 & 運算:(n-1) & hash。但是其實很多雜湊演算法,為了使元素分布均勻,都是用的取模運算,用一個值去模上總長度,即 n%hash。我們知道在計算機中 & 的效率比 % 高很多,那麼如何将 % 轉換為 & 運算呢?在HashMap 中,是用的 (n - 1) & hash 進行運算的,那麼這是為什麼呢?

  這就是本篇部落格我們将要明白的問題。

2、結論

  我們先給出結論:

  當 lenth = 2n 時,X % length = X & (length - 1)

  也就是說,長度為2的n次幂時,模運算 % 可以變換為按位與 & 運算。

  比如:9 % 4 = 1,9的二進制是 1001 ,4-1 = 3,3的二進制是 0011。 9 & 3 = 1001 & 0011 = 0001 = 1

  再比如:12 % 8 = 4,12的二進制是 1100,8-1 = 7,7的二進制是 0111。12 & 7 = 1100 & 0111 = 0100 = 4

  上面兩個例子4和8都是2的n次幂,結論是成立的,那麼當長度不為2的n次幂呢?

  比如:9 % 5 = 4,9的二進制是 1001,5-1 = 4,4的二進制是0100。9 & 4 = 1001 & 0100 = 0000 = 0。顯然是不成立的。

  為什麼是這樣?下面我們來詳細分析。

3、分析過程

  首先我們要知道如下規則:

  ①、"<<" 左移:右邊空出的位上補0,左邊的位将從字頭擠掉,左移一位其值相當于乘2。

  ②、">>"右移:右邊的位被擠掉,右移一位其值相當于除以2。對于左邊移出的空位,如果是正數則空位補0,若為負數,可能補0或補1,這取決于所用的計算機系統。

  ③、">>>"無符号右移,右邊的位被擠掉,對于左邊移出的空位一概補上0。

  根據二進制數的特點,相信大家很好了解。

  對于給定一個任意的十進制數XnXn-1Xn-2....X1X0,我們将其用二進制的表示方法分解:

  XnXn-1Xn-2....X1X0   = Xn*2n+Xn-1*2n-1+......+X1*21+X0*20                       3-1公式

  這裡的十進制數隻有三位,同理當有N位時,後面2的幂次方依次從 0 開始遞增到 N 。

  回到上面的結論: lenth = 2n 時,X % length = X & (length - 1)

  以及對于除法,被除數是滿足配置設定率的(除數不滿足):

  成立:(a+b)÷c=a÷c+b÷c                                                                          3-2公式

  不成立:a÷(b+c)≠a÷c+b÷c

  通過 3-1公式以及 3-2 公式,我們可以得出當任意一個十進制除以一個2k的數時,我們可以将這個十進制轉換成3-1公式的表示形式:

  (XnXn-1Xn-2....X1X0)  / 2k   =  (Xn*2n+Xn-1*2n-1+......+X1*21+X0*20) / 2k = Xn*2n /  2k +Xn-1*2n-1 /  2k  +......+  X1*21 /  2k + X0*20 /  2k

  如果我們想求上面公式的餘數,相信大家一眼就能看出來:

  ①、當 0<= k <= n 時,餘數為 Xk*2k+Xk-1*2k-1+......+X1*21+X0*20   ,也就是說 比 k 大的 n次幂,我們都舍掉了(大的都能整除 2k),比k小的我們都留下來了(小的不能整除2k)。那麼留來下來即為餘數。

  ②、當 k > n 時,餘數即為整個十進制數。

  看到這裡,我們離證明結論已經很近了。再回到上面說的二進制的移位操作,向右移 n 位,表示除以 2n 次方,由此我們得到一個很重要的結論:

  一個十進制數對一個2n 的數取餘,我們可以将這個十進制轉換為二進制數,将這個二進制數右移n位,移掉的這 n 位數即是餘數。

  知道怎麼算餘數了,那麼我們怎麼去擷取這移掉的 n 為數呢?

  我們再看20,21,22....2n  用二進制表示如下:

  0001,0010,0100,1000,10000......

  我們把上面的數字減一:

  0000,0001,0011,0111,01111......

  根據與運算符&的規律,當位上都是 1 時,結果才是 1,否則為 0。是以任意一個二進制數對 2k 取餘時,我們可以将這個二進制數與(2k-1)進行按位與運算,保留的即使餘數。

  這就完美的證明了前面給出的結論:

  當 lenth = 2n 時,X % length = X & (length - 1)

  注意,一定要是2n次方,才滿足上面的公式,否則就是錯誤的。

4、總結

  通過上面的分析過程了,我們完美了證明了公式的正确性。在回到 HashMap 的實作過程,我們知道HashMap的初始容量為啥是 1<<4 了吧,而且每次擴容都是擴大一倍。因為必須要完美的滿足 hash 算法。

作者:

YSOcean

出處:

http://www.cnblogs.com/ysocean/

本文版權歸作者所有,歡迎轉載,但未經作者同意不能轉載,否則保留追究法律責任的權利。

繼續閱讀