在 Java 中,HashMap
是一种高效的数据结构,它通过将键映射到数组中的索引位置来实现快速的插入和查找。但之前看源码总是理解到它要hash之后散列到数组中某一个位置,但却从未深究它究竟怎么散列的,如果不够散那就意味着hash冲突增加,在这个过程中,散列函数的设计至关重要,特别是右移和异或操作如何帮助减少哈希冲突。
- 冲突对性能的影响
- 尽管 HashMap 使用链表和红黑树来处理冲突,但冲突仍然会对性能产生显著影响:
- 查询性能下降:当多个元素存储在同一个桶中时,查找这些元素的时间复杂度可能退化到 O(n),而不是理想情况下的 O(1)。
- 插入性能下降:插入新元素时,如果发生冲突,可能需要遍历链表或调整红黑树,导致插入时间复杂度增加。
- 内存使用效率:冲突频繁发生时,某些桶可能存储过多元素,而其他桶则几乎没有元素,这种不均匀性可能导致内存浪费。
- 如果散列性不足,导致大量冲突,HashMap的性能将受到严重影响。在最坏的情况下,所有元素都可能映射到同一个桶,形成一个链表或红黑树,导致查找、插入和删除操作的时间复杂度退化到 O(n)。
1. HashMap 的基本概念
HashMap
使用一个数组来存储键值对(key-value pairs),每个键通过其哈希值计算出一个数组索引。理想情况下,不同的键应该映射到不同的索引,以避免哈希冲突。
数组:用于存储桶(bucket),每个桶可以存储多个键值对。
链表或红黑树:用于处理哈希冲突,桶中的每个元素可以是链表或红黑树。
1.1 数据结构
HashMap
的核心数据结构如下:
static class Node<K,V> {final int hash; // 哈希值final K key; // 键V value; // 值Node<K,V> next; // 下一个节点
}
2. 计算索引位置的流程
2.1 获取哈希值
当调用 put(key, value)
或 get(key)
方法时,HashMap
首先会通过键的 hashCode()
方法获取哈希值。以下是 put
方法的简化实现:
public V put(K key, V value) {int hash = (key == null) ? 0 : hash(key);// 计算索引位置int index = indexFor(hash, table.length);// 进行插入操作
}
2.2 散列函数的实现
HashMap
中的散列函数是通过 hash
方法实现的。该方法的实现如下:
static final int hash(int h) {h ^= (h >>> 16); // 右移 16 位并异或return h ^ (h >>> 16); // 再次混合
}
2.3 计算索引位置
在 HashMap
中,索引位置的计算是通过以下方式实现的:
int index = (n - 1) & hash;
这里,n
是数组的长度,hash
是经过处理的哈希值。
3. 右移与异或的作用
3.1 为什么需要右移与异或?
在进行哈希值的计算时,HashMap
使用右移和异或的组合来增强哈希值的随机性。让我们通过一个示例来详细分析这一过程。
3.2 示例分析
假设我们有一个键 "key1"
,其哈希值为 0xB2690FF0
。我们将分析右移和异或如何影响哈希值的混合。
原始哈希值
hash: 10110010 01101001 00001111 11110000 (二进制)
3.3 右移操作
首先,我们进行右移 16 位的操作:
hash >>> 16:
原始: 10110010 01101001 00001111 11110000
结果: 00000000 00000000 10110010 01101001
3.4 异或运算
接下来,我们进行异或运算:
new_hash = hash ^ (hash >>> 16);
计算过程
原始哈希值: 10110010 01101001 00001111 11110000
右移结果: 00000000 00000000 10110010 01101001
-----------------------------------------------
new_hash: 10110010 01101001 10110101 10011001
3.5 高位信息的参与
在计算索引时,HashMap
使用以下公式:
if ((p = tab[i = (n - 1) & hash]) == null) {// 插入新节点
}
这里,(n - 1)
是数组长度减去 1 的值。由于数组长度通常是 2 的幂(例如 16、32、64 等),n - 1
的二进制表示通常以 0 结尾,导致在进行按位与运算时,只有低位信息参与计算。
3.6 按位与的影响
因为按位与运算只有在对应的位都为 1 时结果才为 1,若 hash
的高位信息没有参与,可能导致多个不同的哈希值映射到同一个索引位置,从而增加哈希冲突的概率。例如:
- 假设
n = 16
,即n - 1 = 15
,其二进制为0000 1111
。 - 如果
hash
的高位为 0,低位的变化可能导致多个不同的hash
值在与15
进行与运算后得到相同的索引。
3.7 通过右移与异或减少冲突
通过右移和异或,HashMap
能够打破哈希值的模式,使得高位信息参与到最终的哈希计算中。这种方式使得即使在低位相同的情况下,高位的不同也能影响最终的索引计算,从而有效减少哈希冲突的发生。
4. 减少哈希冲突的原理
4.1 高位与低位的混合
通过右移和异或,HashMap
有效地将高位信息引入低位,增强了哈希值的随机性,使得相似的键在哈希表中更可能映射到不同的索引位置。
4.2 均匀分布
理想的散列函数应该能够均匀地分布哈希值。如果哈希值的分布不均匀,某些桶可能会存储过多的元素,从而导致性能下降。通过混合高位和低位,HashMap
提高了哈希值的随机性,使得键值对能够更加均匀地分布在桶中。
4.3 冲突处理机制
尽管 HashMap
通过上述方法减少了哈希冲突,但仍然可能发生冲突。当多个键映射到同一个索引时,HashMap
使用以下两种方法来处理冲突:
a. 链表法
在同一个桶中使用链表存储所有具有相同索引的键值对。当发生冲突时,新元素会被添加到链表的末尾。
b. 红黑树
当某个桶中的元素数量超过一定阈值(如 8),HashMap
会将链表转换为红黑树,以提高查找效率。红黑树的查找时间复杂度为 O(log n),比链表的 O(n) 更高效。
5. 总结
通过对 HashMap
源码的分析,我们可以看到它是如何计算索引位置的,以及散列函数在其中的关键作用。右移和异或的组合使用有效地混合了哈希值的高位和低位,从而减少了哈希冲突的概率,尽可能的确保了 HashMap
的高效性能。如果对你有帮助欢迎点赞支持。