1. 程式人生 > >HashMap中的hash演算法總結

HashMap中的hash演算法總結

前言

演算法一直是我的弱項,然而面試中基本是必考的專案,剛好上次看到一個HashMap的面試題,今天也來學習下 HashMap中的hash演算法是如何實現的。

數學知識回顧

  • << : 左移運算子,num << 1,相當於num乘以2 低位補0
    舉例:3 << 2
    將數字3左移2位,將3轉換為二進位制數字0000 0000 0000 0000 0000 0000 0000 0011,然後把該數字高位(左側)的兩個零移出,其他的數字都朝左平移2位,最後在低位(右側)的兩個空位補零。則得到的最終結果是0000 0000 0000 0000 0000 0000 0000 1100,則轉換為十進位制是12。
    數學意義:
    在數字沒有溢位的前提下,對於正數和負數,左移一位都相當於乘以2的1次方,左移n位就相當於乘以2的n次方。

  • >>: 右移運算子
    舉例:11 >> 2
    則是將數字11右移2位,11 的二進位制形式為:0000 0000 0000 0000 0000 0000 0000 1011,然後把低位的最後兩個數字移出,因為該數字是正數,所以在高位補零。則得到的最終結果是0000 0000 0000 0000 0000 0000 0000 0010。轉換為十進位制是3。
    數學意義:
    右移一位相當於除2,右移n位相當於除以2的n次方。這裡是取商哈,餘數就不要了。

  • >>> : 無符號右移,忽略符號位,空位都以0補齊
    按二進位制形式把所有的數字向右移動對應位數,低位移出(捨棄),高位的空位補零。對於正數來說和帶符號右移相同,對於負數來說不同。 其他結構和>>相似。

  • % : 模運算 取餘
    簡單的求餘運算

  • ^ : 位異或 第一個運算元的的第n位於第二個運算元的第n位相反,那麼結果的第n為也為1,否則為0
    0^0=0, 1^0=1, 0^1=1, 1^1=0

  • & : 與運算 第一個運算元的的第n位於第二個運算元的第n位如果都是1,那麼結果的第n為也為1,否則為0
    0&0=0, 0&1=0, 1&0=0, 1&1=1

  • | : 或運算 第一個運算元的的第n位於第二個運算元的第n位 只要有一個是1,那麼結果的第n為也為1,否則為0
    0|0=0, 0|1=1, 1|0=1, 1|1=1

  • ~ : 非運算 運算元的第n位為1,那麼結果的第n位為0,反之,也就是取反運算(一元操作符:只操作一個數)
    ~1=0, ~0=1

HashMap中的hash演算法

首先要明白一個概念,HashMap中定位到桶的位置 是根據Key的hash值與陣列的長度取模來計算的。
具體的細節我就不說了,預設認為大家都懂這一點。
取模可以改為:hashCode & (length - 1)
看下JDK8中的hash 演算法:

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

首先是取key的hashCode演算法,然後對16進行異或運算和右移運算。
在分析上面異或運算和右移運算問題之前,我們需要先看看另一個事情,什麼呢?就是 HashMap 如何根據 hash 值找到陣列種的物件,我們看看 get 方法的程式碼:

  final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            // 我們需要關注下面這一行
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

我們看看程式碼中註釋下方的一行程式碼:first = tab[(n - 1) & hash])。
使用陣列長度減一 與運算 hash 值。這行程式碼就是為什麼要讓前面的 hash 方法移位並異或。


** 我們分析一下:**
首先,假設有一種情況,物件 A 的 hashCode 為 1000010001110001000001111000000,物件 B 的 hashCode 為 0111011100111000101000010100000。


如果陣列長度是16,也就是 15 與運算這兩個數(前面說的hashCode & (length - 1)), 你會發現結果都是0。這樣的雜湊結果太讓人失望了。很明顯不是一個好的雜湊演算法。


但是如果我們將 hashCode 值右移 16 位,也就是取 int 型別的一半,剛好將該二進位制數對半切開。並且使用位異或運算(如果兩個數對應的位置相反,則結果為1,反之為0),這樣的話,就能避免我們上面的情況的發生。簡而言之就是儘量打亂hashCode的低16位,因為真正參與運算的還是低16位。

不知道這種解釋是否是簡單明瞭,經過自己的思考和分析後 也明白了 這段程式碼設計的初衷,也會感嘆設計者的精妙。


如有疑問歡迎留言,如有遺漏或錯誤的地方也歡迎指出。