散列函数中,选择 31 作为乘数
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
上述的散列函数中, 为什么选择31作为乘数, 又为什么用31就不会丢失信息?
在上述的散列函数中,选择 31 作为乘数有几个原因:
- 31 是一个素数。使用素数作为乘数有助于生成更均匀的哈希值分布。如果使用一个合数作为乘数,特别是较小的合数,那么哈希值可能会更容易出现碰撞,因为生成的哈希值可能在某些模式下重叠。
- 31 是一个奇数。使用奇数作为乘数可以减少信息丢失的风险。如果使用偶数作为乘数,那么在发生乘法溢出时,信息可能会丢失,因为乘以 2 相当于向左移一位。使用奇数作为乘数可以避免这个问题。
- 31 的乘法可以优化。对于许多计算机系统,乘法运算相对较慢。然而,乘以 31 可以通过位操作进行优化。例如,对于一个整数
n
,计算n * 31
可以表示为(n << 5) - n
,即将n
左移 5 位后再减去n
。这种位操作通常比直接进行乘法运算更快。
关于为什么使用 31 不会丢失信息,这实际上是因为 31 是一个奇数。正如前面提到的,当使用偶数作为乘数时,乘法溢出可能导致信息丢失,因为乘以 2 相当于向左移一位。然而,当使用奇数作为乘数时,乘法溢出的影响相对较小,因为奇数乘法不会导致最低位的信息丢失。
总之,选择 31 作为哈希函数中的乘数有助于生成均匀分布的哈希值,并降低信息丢失的风险。同时,31 的乘法还可以通过位操作进行优化,提高计算效率。
奇数乘法不会导致最低位的信息丢失
让我们通过一个二进制数的例子来解释这一点。
假设我们有一个 8 位二进制数 0101 1010
(十进制表示为 90)。
当我们将这个数乘以 2(即左移一位)时,我们得到 1011 0100
(十进制表示为 180)。注意到,原始数字的最低位(最右边的一位)是 0,乘以 2 之后,这个信息被丢失了。原始数字和结果数字之间的关系仅仅是左移了一位,最低位的信息没有保留。
现在,让我们将原始数字乘以一个奇数,比如 3(二进制表示为 11
):
0101 1010 (90)
x 0011 (3)
----------
0101 1010
0101 1010
----------
1111 0110 (270)
可以看到,乘以奇数后的结果是 1111 0110
(十进制表示为 270)。在这个例子中,尽管可能会出现溢出(如果我们仅使用 8 位存储结果),但是我们仍然保留了原始数字的最低位信息(0)。这是因为奇数乘法不会导致最低位的信息丢失。
综上所述,当使用偶数作为乘数时,乘法溢出可能导致信息丢失,因为乘以 2 相当于向左移一位。然而,当使用奇数作为乘数时,乘法溢出的影响相对较小,因为奇数乘法不会导致最低位的信息丢失。
为什么要保证 最低位的信息不丢失
保留最低位信息有助于维护原始数据的一部分特征,从而在哈希计算中保持更好的分布。当我们在哈希函数中使用奇数乘数时,可以避免因为乘法溢出而导致的最低位信息丢失,从而使得哈希函数生成的哈希值具有较好的随机性和分布。
为什么随机性和分布很重要呢?在哈希表(如 HashMap
和 HashSet
)中,哈希值的分布决定了它们的性能。理想情况下,我们希望不同的对象能够产生尽可能不同的哈希值,以减少哈希冲突。当哈希冲突较少时,哈希表的查找、插入和删除操作的性能更高。
当我们使用偶数乘数时,如果发生溢出,最低位信息可能会丢失。这意味着某些原始数据的特征可能会丢失,从而导致生成的哈希值在分布上出现规律性。这可能会增加哈希冲突,从而降低哈希表的性能。
然而,当我们使用奇数乘数时,由于最低位信息得以保留,哈希函数生成的哈希值具有更好的随机性和分布。这有助于减少哈希冲突,提高哈希表的性能。
总之,保留最低位信息可以帮助维持哈希值的随机性和分布,从而提高哈希表的性能。在哈希函数中使用奇数乘数,可以避免因为乘法溢出而导致的最低位信息丢失。