【问题标题】:What is a sensible prime for hashcode calculation?什么是哈希码计算的合理素数?
【发布时间】:2010-12-22 14:24:45
【问题描述】:

Eclipse 3.5 有一个非常好的特性来生成Java hashCode() 函数。它会生成例如(稍微缩短:)

class HashTest {
    int i;
    int j;        
    public int hashCode() {
        final int prime = 31;
        int result = prime + i;
        result = prime * result + j;
        return result;
    }
}

(如果类中有更多属性,则为每个附加属性重复result = prime * result + attribute.hashCode();。对于整数,.hashCode() 可以省略。)

这看起来不错,但对于素数的选择 31。它可能取自hashCode implementation of Java String,它是出于性能原因而使用的,在引入硬件乘法器之后早已不复存在。在这里,对于 i 和 j 的小值,您有许多哈希码冲突:例如 (0,0) 和 (-1,31) 具有相同的值。我认为这是一件坏事(TM),因为小值经常出现。对于 String.hashCode,您还会发现许多具有相同哈希码的短字符串,例如“Ca”和“DB”。如果你取一个大素数,如果你选择素数,这个问题就消失了。

所以我的问题是:什么是好的素数?你用什么标准来找到它?

这是一个一般性问题 - 所以我不想给出 i 和 j 的范围。但我想在大多数应用程序中,相对较小的值比较大的值更频繁地出现。 (如果你有很大的值,那么选择素数可能并不重要。)它可能没有太大的区别,但更好的选择是改进这一点的简单而明显的方法 - 那么为什么不这样做呢? Commons lang HashCodeBuilder 也暗示了奇怪的小值。

(澄清:这不是Why does Java's hashCode() in String use 31 as a multiplier? 的重复,因为我的问题与JDK 31 的历史无关,而是关于会是什么使用相同的基本模板在新代码中获得更好的价值。那里没有一个答案试图回答这个问题。)

【问题讨论】:

  • 31 仍然很好,因为它不一定涉及加载常量。在 ARM 处理器上(大约 99.9997% 的手机至少使用一个)*31 可以在一条指令中执行。实际上,任何奇数,无论是否素数都足够好。
  • 我在考虑桌面程序,无论您选择 31 还是 1327144003 都无关紧要。奇怪的是,在我的机器上乘以 31 实际上要慢一些 - 可能是优化出了问题。 8-)
  • p = (2^n-1) 形式的素数有助于优化编译器通常执行的x * p = (p << n) - p。来自 Joshua Bloch,Effective Java,第 3 章,第 9 项。所以问题 stackoverflow.com/questions/299304/…
  • 并乘以整数 2^n-1, prime, smallish .. 这给了 31。
  • @MarkRotteveel 请注意,这与 [为什么 Java 的 String 中的 hashCode() 使用 31 作为乘数?][1] 完全不同,因为这不是关于 31 的历史,而是关于什么将是一个更好的选择,而不是使用 31,而不使用额外的库或完全不同的计算哈希的方法。那里的答案都没有解决这个问题。 [1]:stackoverflow.com/questions/299304/…

标签: java hashcode primes


【解决方案1】:

我推荐使用 92821。原因如下。

要对此给出有意义的答案,您必须了解ij 的可能值。一般来说,我唯一能想到的是,在许多情况下,小值会比大值更常见。 (15 作为一个值出现在您的程序中的几率比 438281923 好得多。)因此,通过选择适当的素数来使最小的哈希码冲突尽可能大似乎是个好主意。对于 31 这相当糟糕 - 对于 i=-1j=31 已经有了与 i=0j=0 相同的哈希值。

因为这很有趣,所以我编写了一个小程序,在整个 int 范围内搜索这个意义上的最佳素数。也就是说,对于每个素数,我在与0,0 具有相同哈希码的所有i,j 值中搜索Math.abs(i) + Math.abs(j) 的最小值,然后在该最小值尽可能大的地方取素数。

Drumroll:在这个意义上最好的素数是 486187739(最小的碰撞是i=-25486, j=67194)。 92821 几乎一样好且更容易记住,其中最小的冲突是 i=-46272 and j=46016

如果你给“小”另一种含义,并希望尽可能大的碰撞是Math.sqrt(i*i+j*j)的最小值,结果会有点不同:最好是1322837333和i=-6815 and j=70091,但我最喜欢的是92821 (最小碰撞-46272,46016)再次几乎与最佳值一样好。

我确实承认,这些计算在实践中是否有意义还值得商榷。但我确实认为将 92821 作为素数比 31 更有意义,除非你有充分的理由不这样做。

【讨论】:

  • 您正在寻找一个完美哈希值的神奇数字,或者无论如何是一个近乎完美的数字。与这种简单转置的特殊情况相比,我更感兴趣的是看到针对哈希大小的任意输入(例如,8 字节哈希码中的 4 个 2 字节值)的解决方案。
  • 8 字节哈希码?至少在 Java 中这是 4 个字节。无论如何:您可以继续 eclipse hashCode 生成中使用的方案: result = prime * result + i;结果 = 素数 * 结果 + j;等等。因为这个 92821 作为素数可能是一个不错的选择——至少比 eclipse 默认的 31 好很多。
  • 不仅使用一个小的常量是错误的,重复使用它也是错误的,因为你会遇到像newArrayList("a", "bc").hashCode() == newArrayList("ab", "c").hashCode() 这样的冲突(我的例子可能不起作用,但类似的东西会起作用)。
  • @maaartinus 你是对的,有很多更好的哈希算法。我只是想指出一个简单但值得改进的常用简单算法。如果您想要真正好的属性,可以使用更好的库,但这通常是矫枉过正。
  • @ToolmakerSteve 我也怀疑,10% 是否可行。对于一个应用程序来说,付出努力是不可能的。如果我们可以重新设计整个 Java 散列,那么 10% 可能是可以实现的(避免愚蠢的冲突,比如对于任何具有相同键和值的新 Map.Entry,hashCode 为零等),而即使是 0.1% 也可能是值得改进的.
【解决方案2】:

实际上,如果你取一个大到接近INT_MAX 的素数,由于模运算,你会遇到同样的问题。如果您希望主要散列长度为 2 的字符串,那么可能最好在 INT_MAX 的平方根附近使用素数,如果您散列的字符串更长,则没那么重要,无论如何冲突是不可避免的......

【讨论】:

  • 对,模运算使问题变得困难而有趣。我想我会写一个小程序来寻找一个好的解决方案。 :-)
【解决方案3】:

冲突可能不是什么大问题...散列的主要目标是避免使用等号进行 1:1 比较。 如果您有一个实现,其中对于具有冲突哈希的对象,equals“通常”非常便宜,那么这不是问题(根本)。

最后,什么是最好的散列方法取决于你在比较什么。对于 int 对(如您的示例),使用基本的按位运算符就足够了(如使用 & 或 ^)。

【讨论】:

  • 当然没关系,但是改变素数是一种明显而简单的改进方法。那么为什么不这样做呢?
  • 同意。我的主要意思是强调使用素数并不是唯一做事的方式,因为这个问题最终具有非常“通用”的范围。
  • 顺便说一句:使用 && 会非常糟糕,因为这往往会减少每一步之后设置的位数。使用 ^ 更好,但正如有人指出的那样,使用 i ^ j 意味着如果它们相等,则结果为 0,这在直觉上也是一种相当常见的情况。
【解决方案4】:

您需要定义 i 和 j 的范围。两者都可以使用素数。

public int hashCode() {
   http://primes.utm.edu/curios/ ;)
   return 97654321 * i ^ 12356789 * j;
}

【讨论】:

    【解决方案5】:

    我会选择 7243。足够大以避免与小数字发生冲突。不会很快溢出到小数字。

    【讨论】:

    • 我使用前 1000 个素数作为小素数的方便来源primes.utm.edu/lists/small/1000.txt
    • 我不认为溢出很重要 - 如果素数足够大,即使溢出后结果也会很大。我在想像 1327144003 这样的东西。
    【解决方案6】:

    我只想指出哈希码与素数无关。 在JDK实现中

    for (int i = 0; i < value.length; i++) {
                    h = 31 * h + val[i];
                }
    

    我发现如果你把31换成27,结果非常相似。

    【讨论】:

    • 素数是确保每个哈希码都实际发生的一种简单方法,因此如果整数空间广泛分布它们,您就不会浪费任何位。我不太确定是否还有其他优点。但你是对的,27 可能也这样做。所以它和最初的选择 31 一样糟糕——你也会得到非常小的哈希码冲突。 ;-)
    • @Dr.Hans-PeterStörr 对于大小为 2 的幂的哈希表,您所需要的只是一个奇数乘数,无论是否为质数。素数乘数对于素数大小的表格很重要,因为它们没有任何共同因素(除非你不幸使用相同的素数:D)。 AFAIK 在 JDK 中唯一使用素数大小的表是在 String#intern
    • @maaartinus 一个奇数乘数是需要/足够的究竟是什么?正如我所讨论的,哈希码冲突对性能不利,并且小的乘数会产生更多的哈希码冲突,因为属性的小值比大值更有可能。
    • @Dr.Hans-PeterStörr 为了不丢失信息,奇数乘数是必要的(最差的乘数是那些在二进制中以许多零结尾的乘数)。丢失信息显然是不好避免的。 +++ 我们同意小的乘数也不好。 +++ 我的观点是首要性。像m = 101*103*107*109 这样的乘数对于103 大小的哈希表来说是一场灾难(但没有人使用这样的大小)。对于两倍大小的表格,它很可能比31 好得多。那么它可能是对于一个大小与m 互质的表。
    • @maaartinus 是的,这是乘数应该满足的明显属性。我试图指出,如果你看得更远一点,你可以很容易地让它变得更好,并通过多加一点思考来减少哈希码冲突。无论表大小如何,这些都会损害性能。
    猜你喜欢
    • 2018-06-23
    • 2017-02-04
    • 1970-01-01
    • 1970-01-01
    • 2016-11-11
    • 2013-03-24
    • 1970-01-01
    • 2021-11-07
    • 2016-06-23
    相关资源
    最近更新 更多