为什么String中的Java hashCode()使用31作为乘数?

问题描述 投票:444回答:11

根据Java文档,hash code对象的String计算如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

使用int算术,其中s[i]是字符串的第i个字符,n是字符串的长度,^表示取幂。

为什么31用作乘数?

我知道乘数应该是一个相对较大的素数。那么为什么不是29岁,37岁,甚至97岁?

java string algorithm hash
11个回答
381
投票

根据约书亚布洛赫的Effective Java(一本不能推荐的书,我买了这本书得益于不断提到的stackoverflow):

选择值31是因为它是奇数素数。如果它是偶数并且乘法溢出,则信息将丢失,因为乘以2相当于移位。使用素数的优势不太明显,但它是传统的。 31的一个很好的属性是乘法可以用移位和减法代替以获得更好的性能:31 * i == (i << 5) - i。现代VM自动执行此类优化。

(来自第3章,第9项:覆盖等于时始终覆盖哈希码,第48页)


4
投票

布洛赫并没有深入研究这个问题,但我一直听到/相信的理由是这是基本的代数。哈希值可归结为乘法和模数运算,这意味着如果可以提供帮助,您永远不会想要使用具有公共因子的数字。换句话说,相对素数提供了均匀的答案分布。

使用哈希构成的数字通常是:

  • 你把它放入的数据类型的模数(2 ^ 32或2 ^ 64)
  • 哈希表中的桶数的模数(各不相同。在java以前是素数,现在是2 ^ n)
  • 在混音函数中乘以幻数或乘以幻数
  • 输入值

你真的只能控制这些价值,所以需要额外注意。


2
投票

在最新版本的JDK中,仍然使用31。 https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/lang/String.html#hashCode()

哈希字符串的目的是

  • unique(让我们在哈希码计算文档中看到运算符^,它有助于唯一)
  • 便宜的计算成本

31是最大值可以放入8位(= 1字节)寄存器,最大素数可以放入1字节寄存器,是奇数。

乘以31是<< 5然后减去自己,因此需要廉价资源。


0
投票

这是因为31具有一个很好的属性 - 它的乘法可以用比例移位代替,它比标准乘法更快:

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

78
投票

正如Goodrich and Tamassia指出的那样,如果你接受超过50,000个英语单词(形成为两个Unix变体中提供的单词列表的联合),使用常数31,33,37,39和41将产生少于7个冲突案件。知道这一点,许多Java实现选择其中一个常量应该不足为奇。

巧合的是,当我看到这个问题时,我正在阅读“多项式哈希码”部分。

编辑:这里是我上面提到的~10mb PDF书的链接。请参阅Data Structures and Algorithms in Java的10.2哈希表(第413页)一节


55
投票

在(大多数)旧处理器上,乘以31可能相对便宜。例如,在ARM上,它只有一条指令:

RSB       r1, r0, r0, ASL #5    ; r1 := - r0 + (r0<<5)

大多数其他处理器需要单独的移位和减法指令。但是,如果你的乘数很慢,这仍然是一个胜利。现代处理器往往具有快速乘法器,因此它没有太大的区别,只要32在正确的一侧。

它不是一个很好的哈希算法,但它足够好并且比1.0代码更好(并且比1.0规范好得多!)。


30
投票

通过相乘,位向左移位。这使用了更多可用的哈希码空间,减少了冲突。

通过不使用2的幂,也填充低阶最右边的位,以与进入散列的下一个数据混合。

表达式n * 31相当于(n << 5) - n


24
投票

你可以在http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622的“评论”下阅读Bloch的原始推理。他研究了哈希表中得到的“平均链大小”的不同哈希函数的性能。 P(31)是他在K&R的书中找到的那个时期的常见功能之一(但即使是Kernighan和Ritchie也记不住它的来源)。最后他基本上不得不选择一个,所以他采取了P(31),因为它似乎表现得很好。尽管P(33)并不是真的更糟糕,乘以33同样快速计算(只是换了5和加法),他选择了31,因为33不是素数:

在剩下的四个中,我可能选择P(31),因为它是在RISC机器上计算最便宜的(因为31是2的两个幂的差异)。 P(33)计算起来同样便宜,但它的表现稍微差一些,而且33是复合的,这让我有点紧张。

因此,推理并不像这里的许多答案所暗示的那样理性。但是在我们做出决定之后,我们都很善于提出合理的理由(甚至布洛赫可能会倾向于这样做)。


22
投票

实际上,37会很好用! z:= 37 * x可以计算为y := x + 8 * x; z := x + 4 * y。这两个步骤都对应于一个LEA x86指令,因此速度非常快。

事实上,通过设置y := x + 8 * x; z := x + 8 * y,可以以相同的速度乘以更大的素数73。

使用73或37(而不是31)可能会更好,因为它会导致更密集的代码:两个LEA指令只需要6个字节,而7个字节用于移动+移位+减法乘以31个。一个可能的警告是这里使用的3参数LEA指令在英特尔的Sandy桥架构上变得更慢,延迟增加了3个周期。

此外,73是Sheldon Cooper最喜欢的号码。


19
投票

Neil Coffey explains为什么31用于熨平偏见。

基本上使用31为哈希函数提供了更均匀的设置位概率分布。


7
投票

来自JDK-4045622,其中Joshua Bloch描述了选择特定(新)String.hashCode()实施的原因

下表总结了上述各种哈希函数对三个数据集的性能:

1)Merriam-Webster的第二个国际完整词典中的所有单词和短语(311,141个字符串,平均长度为10个字符)。

2)/ bin /,/ usr / bin /,/ usr / lib /,/ usr / ucb /和/ usr / openwin / bin / *(66,304字符串,平均长度为21个字符)中的所有字符串。

3)由昨晚运行了几个小时的网络爬虫收集的URL列表(28,372个字符串,平均49个字符)。

表中所示的性能度量是散列表中所有元素的“平均链大小”(即,与查找元素相比,密钥数量的预期值)。

                          Webster's   Code Strings    URLs
                          ---------   ------------    ----
Current Java Fn.          1.2509      1.2738          13.2560
P(37)    [Java]           1.2508      1.2481          1.2454
P(65599) [Aho et al]      1.2490      1.2510          1.2450
P(31)    [K+R]            1.2500      1.2488          1.2425
P(33)    [Torek]          1.2500      1.2500          1.2453
Vo's Fn                   1.2487      1.2471          1.2462
WAIS Fn                   1.2497      1.2519          1.2452
Weinberger's Fn(MatPak)   6.5169      7.2142          30.6864
Weinberger's Fn(24)       1.3222      1.2791          1.9732
Weinberger's Fn(28)       1.2530      1.2506          1.2439

看看这个表,很明显除了当前的Java函数和Weinberger函数的两个破坏版本之外的所有函数都提供了极好的,几乎无法区分的性能。我强烈推测这种性能本质上是“理论上的理想”,如果您使用真正的随机数生成器代替散列函数,这就是您所获得的。

我排除了WAIS函数,因为它的规范包含随机数的页面,并且它的性能并不比任何更简单的函数更好。其余六个功能中的任何一个看起来都是很好的选择,但我们必须选择一个。我想我会排除Vo的变体和Weinberger的功能,因为它们增加了复杂性,尽管很小。在剩下的四个中,我可能选择P(31),因为它是在RISC机器上计算最便宜的(因为31是2的两个幂的差异)。 P(33)计算起来同样便宜,但它的表现稍微差一些,而且33是复合的,这让我有点紧张。

玩笑


4
投票

我不确定,但我猜他们测试了一些素数样本,并发现31在一些可能的字符串样本中得到了最好的分布。

© www.soinside.com 2019 - 2024. All rights reserved.