为啥在 hashCode 中使用素数?

Posted

技术标签:

【中文标题】为啥在 hashCode 中使用素数?【英文标题】:Why use a prime number in hashCode?为什么在 hashCode 中使用素数? 【发布时间】:2011-04-06 12:00:28 【问题描述】:

我只是想知道为什么在类的hashCode() 方法中使用素数?例如,当使用 Eclipse 生成我的 hashCode() 方法时,总是使用素数 31

public int hashCode() 
     final int prime = 31;
     //...

参考资料:

这是一个很好的 Hashcode 入门和我发现的关于散列如何工作的文章(C#,但概念是可转移的): Eric Lippert's Guidelines and rules for GetHashCode()

【问题讨论】:

相关:Why does Java's hashCode() in String use 31 as a multiplier? 这或多或少是问题***.com/questions/1145217/…的重复。 请在***.com/questions/1145217/…查看我的答案。它与字段(不是环!)上多项式的属性有关,因此与素数有关。 【参考方案1】:

听说选择 31 是为了让编译器可以优化乘法到左移 5 位然后减去这个值。

【讨论】:

编译器如何优化这种方式? x*31==x*32-1 并不是所有 x 都为真。您的意思是左移 5(等于乘以 32),然后减去原始值(在我的示例中为 x)。虽然这可能比乘法更快(顺便说一下,它可能不适用于现代 cpu 处理器),但在为哈希码选择乘法时需要考虑更重要的因素(想到将输入值平均分配到存储桶) 搜索一下,这是一个很普遍的看法。 普通意见无关紧要。 @Grizzly,它比乘法快。 IMul​​ 在任何现代 CPU 上的最小延迟为 3 个周期。 (参见 agner fog 的手册)mov reg1, reg2-shl reg1,5-sub reg1,reg2 可以在 2 个周期内执行。 (mov 只是一个重命名,需要 0 个周期)。【参考方案2】:

它通常有助于在哈希桶中实现更均匀的数据分布,尤其是对于低熵键。

【讨论】:

【参考方案3】:

这是一个更接近源的citation。

归结为:

31 是素数,可以减少冲突 31 产生良好的分布,具有 速度的合理权衡

【讨论】:

【参考方案4】:

因为您希望乘以的数字和插入的桶数具有正交素因数分解。

假设有 8 个桶要插入。如果您用来乘以的数字是 8 的倍数,则插入的存储桶将仅由最不重要的条目(根本不相乘的条目)确定。类似的条目会发生冲突。不适合散列函数。

31 是一个足够大的素数,桶的数量不可能被它整除(事实上,现代 java HashMap 实现将桶的数量保持为 2 的幂)。

【讨论】:

然后乘以 31 的哈希函数将执行非最优。但是,考虑到 31 作为乘数的常见程度,我会认为这样的哈希表实现设计不佳。 所以选择 31 是基于哈希表实现者知道哈希码中常用 31 的假设? 31 的选择是基于大多数实现具有相对较小素数的分解的想法。通常是 2s、3s 和 5s。它可能从 10 开始,当它太满时增长 3 倍。大小很少是完全随机的。即使是这样,30/31 也不是坏的几率,因为它具有良好同步的哈希算法。正如其他人所说,它也可能很容易计算。 换句话说......我们需要了解输入值的集合和集合的规律性,以便编写一个旨在去除这些规律性的函数,因此set 不会在同一个哈希桶中发生冲突。用素数乘/除/取模可以达到这种效果,因为如果你有一个带有 X 项的循环并且你在循环中跳过 Y 空间,那么在 X 成为 Y 的一个因素之前你永远不会回到同一个位置. 由于 X 通常是偶数或 2 的幂,所以你需要 Y 是素数,所以 X+X+X... 不是 Y 的因数,所以 31 耶! :// @FrankQ。这是模运算的本质。 (x*8 + y) % 8 = (x*8) % 8 + y % 8 = 0 + y % 8 = y % 8【参考方案5】:

选择素数是为了在哈希桶之间最好地分配数据。如果输入的分布是随机且均匀分布的,则哈希码/模数的选择无关紧要。仅当输入具有某种模式时才会产生影响。

在处理内存位置时经常出现这种情况。例如,所有 32 位整数都与可被 4 整除的地址对齐。查看下表,直观了解使用素数与非素数模数的效果:

Input       Modulo 8    Modulo 7
0           0           0
4           4           4
8           0           1
12          4           5
16          0           2
20          4           6
24          0           3
28          4           0

注意使用素数模数与非素数模数时几乎完美的分布。

然而,尽管上面的例子在很大程度上是人为的,但一般原则是,在处理输入模式时,使用素数模数将产生最佳分布。

【讨论】:

我们不是在谈论用于生成哈希码的乘数,而不是用于将这些哈希码分类到桶中的模数吗? 同理。在 I/O 方面,散列输入散列表的模运算。我认为关键是如果你乘以素数,你会得到更多随机分布的输入,以至于模数甚至不重要。由于散列函数弥补了更好地分配输入的不足,使它们不那么规则,因此无论用于将它们放入存储桶中的模数如何,它们都不太可能发生冲突。 这种答案非常有用,因为它就像教别人如何钓鱼,而不是为他们抓鱼。它可以帮助人们看到理解使用素数作为散列的基本原理......这是不规则地分配输入,以便它们在模数后均匀地落入桶中:)。跨度> 这应该是答案。上述 cmets 中的后续问题也非常好(关于为什么质数是乘数还是模数本质上没有太大区别)。【参考方案6】:

对于它的价值,Effective Java 2nd Edition 放弃了数学问题,只是说选择 31 的原因是:

因为它是一个奇怪的素数,而且使用素数是“传统的” 它也是 2 的幂次方,允许按位优化

这是完整的引用,来自第 9 条:当您覆盖 equals 时,请始终覆盖 hashCode

选择值 31 是因为它是一个奇数素数。如果它是偶数并且乘法溢出,信息将会丢失,因为乘以 2 相当于移位。使用素数的优势不太明显,但它是传统的。

31 的一个很好的特性是乘法可以替换为移位 (§15.19) 和减法以获得更好的性能:

 31 * i == (i << 5) - i

现代虚拟机自动进行这种优化。


虽然本项目中的配方产生了相当好的哈希函数,但它并没有产生最先进的哈希函数,Java 平台库在 1.6 版中也没有提供这样的哈希函数。编写此类哈希函数是一个研究课题,最好留给数学家和理论计算机科学家。

也许该平台的后续版本将为其类和实用方法提供最先进的散列函数,以允许普通程序员构建此类散列函数。同时,本项目中描述的技术应该足以满足大多数应用程序的需求。

相当简单地说,可以说使用具有多个除数的乘数将导致更多hash collisions。因为为了有效的散列,我们希望最小化冲突的数量,我们尝试使用具有更少除数的乘法器。根据定义,素数恰好有两个不同的正除数。

相关问题

Java hashCode from one field - 配方,以及使用 Apache Commons Lang 构建器的示例 is it incorrect to define an hashcode of an object as the sum, multiplication, whatever, of all class variables hashcodes? Absolute Beginner's Guide to Bit Shifting?

【讨论】:

嗯,但是有许多合适的 素数 要么是 2^n + 1(所谓的 费马素数),即3, 5, 17, 257, 655372^n - 1 (梅森素数):3, 7, 31, 127, 8191, 131071, 524287, 2147483647。但是选择了31(而不是127)。 “因为它是一个奇数素数” ...只有一个偶数素数:P 我不喜欢“Effective Java”中“不太清楚,但很传统”的措辞。如果他不想深入研究数学细节,他应该写一些类似“有[相似]数学原因”的东西。他的写作方式听起来只有历史背景:(【参考方案7】:

首先,您计算以 2^32 为模的哈希值(int 的大小),因此您需要与 2^32 相对质数的值(相对质数意味着没有公约数)。任何奇数都可以。

然后对于给定的哈希表,索引通常是根据哈希值以哈希表的大小为模计算得出的,因此您需要与哈希表的大小相对质数的东西。出于这个原因,通常选择哈希表的大小作为素数。在 Java 的情况下,Sun 实现确保大小始终是 2 的幂,因此在这里奇数也足够了。还对哈希键进行了一些额外的按摩,以进一步限制冲突。

如果哈希表和乘数有一个公共因子n 的不良影响可能是在某些情况下,哈希表中只有 1/n 个条目会被使用。

【讨论】:

【参考方案8】:

31 也特定于使用 int 作为哈希数据类型的 Java HashMap。因此最大容量为 2^32。使用更大的费马或梅森素数是没有意义的。

【讨论】:

【参考方案9】:

使用素数的原因是为了在数据表现出某些特定模式时最大限度地减少冲突。

首先要做的是:如果数据是随机的,则不需要素数,您可以对任何数字进行 mod 运算,并且对于模数的每个可能值,您将有相同数量的冲突。

但是当数据不是随机的时,就会发生奇怪的事情。例如,考虑始终是 10 的倍数的数字数据。

如果我们使用 mod 4,我们会发现:

10 模 4 = 2

20 模 4 = 0

30 模 4 = 2

40 模 4 = 0

50 模 4 = 2

所以从模数 (0,1,2,3) 的 3 个可能值中,只有 0 和 2 会发生冲突,这很糟糕。

如果我们使用像 7 这样的素数:

10 模 7 = 3

20 模 7 = 6

30 模 7 = 2

40 模 7 = 4

50 模 7 = 1

我们还注意到 5 不是一个好的选择,但 5 是质数,原因是我们所有的键都是 5 的倍数。这意味着我们必须选择一个不会分割我们的键的质数,选择一个大的素数通常就足够了。

所以在重复性方面犯了错误,使用质数的原因是为了抵消散列函数冲突分布中键中模式的影响。

【讨论】:

以上是关于为啥在 hashCode 中使用素数?的主要内容,如果未能解决你的问题,请参考以下文章

Java中hashcode的计算方式

Java中hashcode的计算方式

Java中hashcode的计算方式

Java中hashcode的计算方式

equals和hashcode

hashCode对Map的影响