《HashMap中位運算的探索》

1.哈希表中尋址操作:tab[i = (n - 1) & hash]

  • 尋址就是找到即將被put進哈希表的元素在哈希表的具體下標位置
  • 從下面的源碼可以看出n-1代表哈希表的最大下標,hash代表key經歷散列函數的值
  • 那麼尋址操作如何保證下標不越界呢?
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
}            
  • 代入參數來驗證這段代碼是如何保證下標不越界,爲了方便封裝一個方法
  public static int indexFor(int hash,int hashTableLen){
       return (hashTableLen-1) & hash;
  }
  
  public static void main(String[] args) {
        int hash= "ab553cu2k".hashCode();//1127860029
        int index= indexFor(hash, 16);//初始容量 16
 }
第一步 1127860029轉換成二進制: 0100 0011 0011 1001 1100 0111 0011 1101
第二步 代入indexFor函數 即   
			0100 0011 0011 1001 1100 0111 0011 1101 & 
			0000 0000 0000 0000 0000 0000 0000 1111     
第三步得到結果0000 0000 0000 0000 0000 0000 0000 1101 等於13
  • 可以得出結論尋址操作就是利用按位與的特性:全部是1纔是1,來保證不會出現下標越界的情況,比如上面按位與出來的結果永遠不會超過15,如果給定的哈希表長度是16的話

2.散列函數

  • 爲什麼不直接用key的hashcode呢?還要把key的哈希值無符號右移16位再取異或key的哈希值?
	public static int hash(Object key) {
	   int h;
	   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
	}
那上面的例子來說,如果拿key的哈希值去確定該元素所在哈希表的位置,
那麼最終會調用tab[i = (n - 1) & hash],結果如下
key的哈希值:    0100 0011 0011 1001 1100 0111 0011 1101 & 
			   0000 0000 0000 0000 0000 0000 0000 1111     
               0000 0000 0000 0000 0000 0000 0000 1101 等於13
從上面的過程可以發現key的哈希值的高16位並不會影響到最終的結果,只有低16會影響到結果
     
如果對key進行該操作:(h = key.hashCode()) ^ (h >>> 16)
   首先對key的哈希值無符號右移16位(高位補0)得到 
   0000 0000 0000 0000 0100 0011 0011 1001 
   再異或key的哈希值
    0000 0000 0000 0000 0100 0011 0011 1001 ^
    0100 0011 0011 1001 1100 0111 0011 1101
    0100 0011 0011 1001 1000 0100 0000 0100 (結果)
    這樣做的目的:讓它的低16位擁有了高16位的特性,從而避免了大量元素落到哈希表同一位置

打個比方:
     0100 0111 0011 1001 1100 0111 0011 11010100 0011 0011 1001 1100 0111 0011 11010100 0001 0011 1001 1100 0111 0011 1101    
    分別按位與上一個默認容量(16-10000 0000 0000 0000 0000 0000 0000 1111
    他們的結果與高16位無關                   
  • 所以得出結論:散列函數這樣做的目的是爲了更好的均勻分配元素在哈希表的位置

3.tableSizeFor方法

  • 給定一個初始化容量,返回一個大於等於該值的2的次方數,作爲最終哈希表初始化容量
 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    
   public static  int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1; 
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= (1 << 30) ? (1 << 30) : n + 1);
    }
   1.假設在初始化HashMap的時候給定的初始化cap是1818-1=17
    換算成二進制那麼就是       0001 0001
   2.經過如上位運算,最終n變成 0001 1111 等於31
   3.最後再返回31+1 = 32 也就是25次方
  • 多代入幾次你會發現 tableSizeFor方法其實在把給定二進制的低位全部改成1變成單數,最後加1 變成爲2的 次方數,最終返回

4.HashMap爲什麼要用tableSizeFor方法保證哈希表容量一定是2的冪?

  • 哈希碰撞兩個不同key經過散列函數hash計算後得到了相同的結果
  • 當把鍵值對put進哈希表中的時候會調用 (n - 1) & hash 表達式,即哈希表最大下標按位與上1個key經歷散列函數的值,以此來確定該元素在哈希表的下標位置
假設現有兩個key的哈希值分別爲0100101101

==============當n不爲2的次方數時=============

假設n爲17 那麼 n-1的二進制表示爲 10000
然後分別按位與上key的hash值
 10000 & 
 01001  ===00000 
 
 10000 & 
 01101  ===00000 


===============當n爲2的次方數時===============

假設n爲16 那麼 n-1的二進制表示爲01111
然後分別按位與上key的hash值
01111 &
01001 ===01001

01111 &
01101 ===01101 
  • 它的想法是讓(n-1)的二進制擁有更多的1來與上key的哈希值帶來更大的變數
  • 可以看出當n(哈希表容量)等於2的次方的時候,能更有效的減少hash碰撞,使得元素更均勻的分佈在哈希表上
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章