《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的次方的时候,能更有效的避免大量元素落在同一个哈希表位置,使得元素更均匀的分布在哈希表上
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值