HashMap中的hash算法

文章探讨了Hash表的核心概念,包括哈希函数如何将key映射到数组下标,以及由此产生的哈希冲突问题。重点分析了HashMap中的hash算法,尤其是(h=h^key.hashCode())^(h>>>16)这一部分,解释了为何使用异或运算来优化散列分布,以减少哈希冲突。同时,文章指出HashMap的容量建议为2的幂次方以优化索引计算并降低冲突概率。

一、关于Hash表和Hash函数
Hash表也称散列表,直译为哈希表,hash表是一种根据关键字值(key-value)而直接进行访问的数据结构。在哈希表的键值对关系中,key到value中间还存在着一个映射值,这个映射值就是数组的下标index,key正是通过映射到数组对应的下标index而访问到value值的,但key又是如何映射到数组下标的呢?这就要通过一个映射函数f(key),这个函数我们称之为哈希函数

哈希表中,每个key通过哈希函数的计算都会得到一个唯一的index,但并不是每个index都对应一个唯一的key,就是说可能有两个以上的key映射到同一个index,这就产生了哈希冲突的问题,本文讨论的重点不是如何解决哈希冲突,而是基于HashMap的实现来分析一下,如何通过优化hash算法来减少哈希冲突

一个好的hash算法,应该是尽量避免不同的key映射出相同的index,这样才能减少哈希冲突的出现。比如在HashMap中解决哈希冲突采用的是拉链法,这种方法把冲突于某个数组下标的数据都保存到对应数组单元中一个链表中,如下图所示,这种数据结构中数组单元保存不是单一的数值,而是一个链表。按照这种方式,如果哈希冲突越多,可能造成数组的利用率就越低,因为有些数组单元可能被闲置,而数组单元上的链表可能会越大,这势必影响到Map的性能,所以尽可能地避免哈希冲突很重要

当然从另外一个角度来说,哈希冲突是不可避免的,比如一个HashMap的容量为16,现有20个元素要存入到这个Map中,在容量不扩展的情况下,要把20个元素存入容量只有16的HashMap中至少会产生4次哈希冲突,注意了这里是至少4次,但即使无法避免冲突,我们还是要让哈希函数的映射值尽量分散,这样才能尽量减少哈希冲突,提高数组的利用率,避免数组单元的链表过大

二、HashMap的hash算法
来看看HashMap中的hash算法是如何实现的

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
 
//获取index的方法,1.7源码,1.8中用tab[(n - 1) & hash]代替,但原理一样
static int indexFor(int h, int length) {
    return h & (length-1);
}
index的获取很简单,就是把h对 length-1取模,其中length为数组长度,所以关键的是h是怎么计算得到的,也很简单,就是通过 h = (h = key.hashCode()) ^ (h >>> 16)得到的,看似一个简单的公式,其实里面的学问也不小,所以这里的主要疑问是为什么h的计算方法是 h = (h = key.hashCode()) ^ (h >>> 16)

1. h & (length-1)

在公式 h & (length-1)中,其中length为数组的长度,HashMap的数组默认长度是16,因为大多数情况来说数组长度都不会太大,所以影响 h & (length-1)计算结果的主要因素就是h的低位数据了,也就是大多数情况下h的高位数据对计算结果是没有影响的。比如只有当数组长度length大于2^16即大于65536的时候,h的高16位才会对 h & (length-1)的计算结果产生影响,所以在HashMap中我们也主要对h的低16位进行优化,也就是让h的低16位的数据尽量分散,上面的 (h = key.hashCode()) ^ (h >>> 16)算法也是基于这个目的而设计的

2.为什么是(h = key.hashCode()) 和 (h >>> 16)异或

首先 h = key.hashCode()是key对象的一个hashCode,每个不同的对象其哈希值都不相同,其实底层是对象的内存地址的散列值,所以最开始的h是key对应的一个整数类型的哈希值

h >>> 16的意思是将h右移16位,然后高位补0,然后再与(h = key.hashCode()) 异或运算得到最终的h值,为什么是异或运算呢?当然我们知道目的是为了让h的低16位更有散列性,但为什么是异或运算就更有散列性呢?而不是与运算或者或运算呢?网上大多的文章都没有给出一个很好的说明,这里我将证明一下为什么异或就能够得到更好散列性

3.为什么异或运算的散列性更好

先来看一下下面的这组运算

上面是将0110和0101分别进行与、或、异或三种运算得到不同的结果,我们主要来看计算的过程:

与运算:其中1&1=1,其他三种情况1&0=0, 0&0=0, 0&1=0 都等于0,可以看到与运算的结果更多趋向于0,这种散列效果就不好了,运算结果会比较集中在小的值

或运算:其中0&0=0,其他三种情况 1&0=1, 1&1=1, 0&1=1 都等于1,可以看到或运算的结果更多趋向于1,散列效果也不好,运算结果会比较集中在大的值

异或运算:其中0&0=0, 1&1=0,而另外0&1=1, 1&0=1 ,可以看到异或运算结果等于1和0的概率是一样的,这种运算结果出来当然就比较分散均匀了

总的来说,与运算的结果趋向于得到小的值,或运算的结果趋向于得到大的值,异或运算的结果大小值比较均匀分散,这就是我们想要的结果,这也解释了为什么要用异或运算,因为通过异或运算得到的h值会更加分散,进而 h & (length-1)得到的index也会更加分散,哈希冲突也就更少

4.HashMap的容量为什么建议是2的幂次方

2的幂次方是指数组长度length的大小,假如length等于2的幂次方,那样length-1的二进制数据的低位就全部为1了,比如当数组长度为16,那么15的二进制就为1111,只有这样,在计算数组下标index的时候才能更好地利用h的散列性,举个例子:

比如 length-1=15,二进制即为1111,分别跟三个不同的h值进行与运算,计算如下

1111 & 101010100101001001000 结果:1000 = 8

1111 & 101000101101001001001 结果:1001 = 9

1111 & 101010101101101001010 结果:1010 = 10

1111 & 101100100111001101100 结果: 1100 = 12

但是如果length为11的话,那么length-1的二进制则表示为1010,与同样的三个h值与运算,计算如下

1010 & 101010100101001001000 结果:1000 = 8

1010 & 101000101101001001001 结果:1000 = 8

1010 & 101010101101101001010 结果:1010 = 10

1010 & 101100100111001101100 结果: 1000 = 8

很明显,当数组长度为16的时候,没有产生哈希冲突,而为11的时候,产生了3次哈希冲突,所以这就说明了为什么HashMap的容量建议为2的幂次方


版权声明:本文为优快云博主「 十 月」的原创文章
原文链接:https://blog.youkuaiyun.com/liuxingrong666/article/details/103640412

HashMap哈希算法和冲突解决机制是其高效性能的核心所在。以下是其原理与实现细节的详细解析: ### 哈希算法实现原理 HashMap 使用一种将键(Key)映射到数组索引的哈希函数,以实现快速的数据访问。该过程包括两个主要步骤:**哈希值计算** 和 **索引计算**。 1. **哈希值计算**: - HashMap 首先调用键对象的 `hashCode()` 方法获取一个整数形式的哈希码。 - 为了减少哈希碰撞的概率,HashMap 对原始哈希码进行扰动处理。具体来说,通过右移和异或操作来提高低位的随机性[^1]。 ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 2. **索引计算**: - 扰动后的哈希值会被进一步处理,以确定它在哈希表中的存储位置。通常使用位掩码运算来代替取模运算,以提升效率。假设当前哈希表的容量为 `n`,则索引值由以下公式计算得出: ```java index = hash & (n - 1) ``` - 这种方式要求哈希表的容量始终是 2 的幂次方,从而保证 `n - 1` 的低位全为 1,使得哈希分布更加均匀。 ### 冲突解决方法 由于不同的键可能会生成相同的哈希值,导致它们被映射到同一个索引上,这就是所谓的“哈希冲突”。HashMap 主要采用 **链地址法(Chaining)** 来解决这一问题,并在特定条件下优化为红黑树结构。 1. **链地址法(Chaining)**: - 每个哈希桶中维护一个链表,用于存储所有发生冲突的键值对。 - 在 Java 8 之前,所有的冲突都通过链表处理。 - 插入时,新的键值对会被追加到对应桶的链表末尾;查找时,则遍历链表直到找到匹配项[^3]。 2. **链表转红黑树**: - 当某个桶中的链表长度超过阈值(默认为 8)时,链表会被转换为红黑树结构。 - 红黑树是一种自平衡二叉搜索树,能够在 O(log n) 时间复杂度内完成插入、删除和查找操作,显著提升了高冲突情况下的性能。 - 如果桶中的元素数量减少到一定程度(默认为 6),红黑树会重新退化为链表,以节省内存开销。 3. **扩容机制**: - 当哈希表中的元素数量超过负载因子(Load Factor,默认为 0.75)乘以当前容量时,HashMap 会自动扩容(通常是翻倍)。 - 扩容后,原有的键值对需要重新计算索引并分配到新的桶中,这个过程称为“再哈希”(Rehashing)。 - 扩容虽然代价较高,但能有效降低哈希冲突率并维持较高的查询效率[^4]。 ### 总结 HashMap哈希算法结合了高效的哈希扰动和索引计算策略,而其冲突解决机制则基于链地址法,并在必要时引入红黑树进行优化。这种设计使得 HashMap 在大多数场景下都能保持接近 O(1) 的时间复杂度,适用于大规模数据的快速存取需求。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值