为了优化HashMap的查询效率并减少哈希冲突,需要尽量使键值对均匀分布在不同的桶中。理想情况下,哈希函数能为每个键值对生成一个均匀分布的哈希码,从而最小化哈希冲突的发生。然而,Java的哈希码取值范围为-2147483648到2147483647,这将近40亿的散列空间。直接使用该范围内的哈希码作为存储索引是不现实的,因为那需要一个巨大的数组,内存消耗将是不可接受的。
因此,在实际应用中,通常将哈希码对数组长度取模,所得余数作为存储桶的索引。这种方法将键值对均匀分布到有限的桶区间内,在充分利用内存的同时,也有效缓解了哈希冲突的问题。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)
那为什么数组下标的计算方法不是hash%n呢?
(n - 1) & hash 等于 hash % n 的原因是因为它们都能够获取hash值在一定范围内的余数,区别在于计算方式不同。
在计算机中,通常使用&运算来快速获取一个数的最后几位二进制位。n - 1如果是2的幂次减1,那么它的二进制表示就是由n个1组成的数,例如7(111二进制)。当 (n-1) & hash时,结果就是保留hash值的后n位。而hash % n则是用常规的求余运算来计算hash在n范围内的余数。
由于(n-1)&运算实际上是在二进制级获取末尾n位的快捷方式,而%运算则是十进制求余,两者在计算结果上是等价的。
不过,位运算&由于直接对内存的二进制位进行操作,它的运算效率比%高得多,所以在Hash求余的算法中一般优先选择&。但是,要使(n-1)&hash等于hash%n,需要满足n是2的幂次数,否则&运算得到的结果就不等于求余了。因此,在设计Hash求余算法时,常常会选择一个接近2的幂次的质数作为n的值。
总之,(n-1)&hash等于hash%n,前提是n是2的幂次数,它利用了位运算的高效性来加速求余过程。
举个例子:
假设你有一个储物柜,格子数量是16个(2的4次方)。格子从0到15编号。你手上有很多东西,每件东西都有一个号码(相当于hash值)。
你决定按照下面规则,将东西放进储物柜的格子里存放:
- 用除法求余运算: 东西号码 % 16 = 余数 将该东西放进编号为"余数"的格子
- 用位运算: (16-1)用二进制表示为1111 (16-1) & 东西号码 = 余数
将该东西放进编号为"余数"的格子
不管用哪种方式,东西都会被均匀分布到0到15的格子里。由于格子数16是2的幂次数,两种运算得到的余数结果是完全相同的。
位运算的方式实际上是先把东西号码的二进制码限制在最后4位(余数最多只能是4位二进制),这4位二进制数字对应的就是0到15的余数。
而当格子数量不是2的幂次时,比如格子数是15,第二种位运算(15-1)&东西号码就不等于第一种求余号码%15了。
所以(n-1) & hash 等于 hash % n的前提,就是n必须是2的幂次数。这样可以保证两种运算得到的余数是一致的,从而可以被均匀分布到n个位置。