【问题标题】:Proof: why does java.lang.String.hashCode()'s implementation match its documentation?证明:为什么 java.lang.String.hashCode() 的实现与其文档匹配?
【发布时间】:2010-10-23 18:27:39
【问题描述】:

java.lang.String.hashCode()famously 的 JDK 文档说:

String 对象的哈希码计算为

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

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

这个表达式的标准实现是:

int hash = 0;
for (int i = 0; i < length; i++)
{
    hash = 31*hash + value[i];
}
return hash;

看着这让我觉得我在学习算法课程时正在睡觉。该数学表达式如何转换为上面的代码?

【问题讨论】:

    标签: java algorithm math hashcode


    【解决方案1】:

    所有字符中算出String的hashcode难道一点用都没有吗?想象一下将完整路径放入 HashSet 的文件名或类名。或者因为“HashSet always beats Lists”而使用字符串文档的哈希集而不是列表的人。

    我会这样做:

    int off = offset;
    char val[] = value;
    int len = count;
    
    int step = len <= 10 ? 1 : len / 10;
    
    for (int i = 0; i < len; i+=step) {
       h = 31*h + val[off+i];
    }
    hash = h
    

    最后,哈希码只是一个提示。

    【讨论】:

    • 忽略字符串中的一半字符意味着将“计数字符串”序列存储到哈希表中很容易导致 100 个字符串映射到每个哈希值。忽略一半以上的角色会让事情变得更糟。出于散列目的而忽略字符串的任何方面都会冒着巨大的损失,以换取很小的回报。
    • 这本质上是java的早期设计者。最初,当字符串长度超过 15 个字符时,字符串散列函数仅提取字符样本。最终它不得不被修复,因为它在某些字符串中产生了非常糟糕的哈希性能(例如,使用一组通常看起来相似的 URL):bugs.java.com/bugdatabase/view_bug.do?bug_id=4045622。不使用整个字符串的性能提升并不能抵消更差的哈希性能。
    • 澄清一下:第二种性能是指“哈希表”性能,而不是计算哈希的原始速度。
    【解决方案2】:

    展开循环。然后你得到:

    int hash = 0;
    
    hash = 31*hash + value[0];
    hash = 31*hash + value[1];
    hash = 31*hash + value[2];
    hash = 31*hash + value[3];
    ...
    return hash;
    

    现在你可以做一些数学运算,为初始哈希值插入 0:

    hash = 31*(31*(31*(31*0 + value[0]) + value[1]) + value[2]) + value[3])...
    

    再简化一下:

    hash = 31^3*value[0] + 31^2*value[1] + 31^1*value[2] + 31^0*value[3]...
    

    这基本上就是给出的原始算法。

    【讨论】:

    • 您可能想用静态单一分配 (SSA) 形式来解释它,这样就无需考虑“哈希”在任何给定时间点具有什么值。 :-)
    • 看起来原来的算法说应该是:31^3*value[0] + 31^2*value[1] + 31^1*value[2] + ... 或者是只是我油炸的大脑失灵了?
    【解决方案3】:

    我不确定您是否错过了该文档中“^ 表示求幂”(不是 xor)的位置。

    每次循环,hash 的前一个值再次乘以 31,然后再添加到 value 的下一个元素。

    人们可以通过归纳证明这些事情是相等的,但我认为一个例子可能更多 明确:

    假设我们正在处理一个 4 字符的字符串。让我们展开循环:

    hash = 0;
    hash = 31 * hash + value[0];
    hash = 31 * hash + value[1];
    hash = 31 * hash + value[2];
    hash = 31 * hash + value[3];
    

    现在通过将散列的每个值代入以下语句,将它们组合成一个语句:

    hash = 31 * (31 * (31 * (31 * 0 + value[0]) + value[1]) + value[2])
         + value[3];
    

    31 * 0 为 0,所以简化:

    hash = 31 * (31 * (31 * value[0] + value[1]) + value[2])
         + value[3];
    

    现在将两个内项乘以第二个 31:

    hash = 31 * (31 * 31 * value[0] + 31 * value[1] + value[2])
         + value[3];
    

    现在将三个内部项乘以前 31:

    hash = 31 * 31 * 31 * value[0] + 31 * 31 * value[1] + 31 * value[2]
         + value[3];
    

    并转换为指数(不再是真正的 Java):

    hash = 31^3 * value[0] + 31^2 * value[1] + 31^1 * value[2] + value[3];
    

    【讨论】:

    • RE 你的第一句话:你有没有看到一些证据表明问题或特定答案是假设异或?
    • 您对代码和文档如何等效表示困惑。由于文档使用“^”进行幂运算,但 Java 通常使用它来表示按位异或,我想知道这是否是您混淆的根源。 (当我开始写我的答案时,没有其他答案,顺便说一句)
    • 啊,我明白了。不,我知道这是取幂,但不清楚如何从数学表达式中遵循实现。您的回答大大阐明了这一点-但是知道仅给定该表达式就编写该代码对我来说仍然是一个飞跃。要获得该代码,您似乎必须编写一个小示例,意识到您可以在最内层嵌套中“以巧妙的方式乘以 0”以完成模式,然后形成循环。跨度>
    • 如果代码真的在前,文档是在后写的,我一点也不奇怪。
    【解决方案4】:

    归纳证明:

    T1(s) = 0 if |s| == 0, else s[|s|-1] + 31*T(s[0..|s|-1])
    T2(s) = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
    P(n) = for all strings s s.t. |s| = n, T1(s) = T2(s)
    
    Let s be an arbitrary string, and n=|s|
    Base case: n = 0
        0 (additive identity, T2(s)) = 0 (T1(s))
        P(0)
    Suppose n > 0
        T1(s) = s[n-1] + 31*T1(s[0:n-1])
        T2(s) = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] = s[n-1] + 31*(s[0]*31^(n-2) + s[1]*31^(n-3) + ... + s[n-2]) = s[n-1] + 31*T2(s[0:n-1])
        By the induction hypothesis, (P(n-1)), T1(s[0:n-1]) = T2(s[0:n-1]) so
            s[n-1] + 31*T1(s[0..n-1]) = s[n-1] + T2(s[0:n-1])
        P(n)
    

    我想我知道了,并且要求提供证明。

    【讨论】:

      【解决方案5】:

      看看前几次迭代,你会看到模式开始出现:

      哈希0 = 0 + s0 = s0 哈希1 = 31(hash0) + s1 = 31(s0) + s 1 哈希2 = 31(hash1) + s2 = 31(31(s0) + s1) + s2 = 312(s0) + 31(s1) + s2 ...

      【讨论】:

      • 如果你能垂直对齐所有对应的术语,并在第三行分配 31(...) 会更好。
      • @CookieOfFortune:它有一个 HTML 标签。查看页面源代码。不过,我会使用 Unicode。
      猜你喜欢
      • 2013-02-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-28
      • 2011-10-09
      • 1970-01-01
      相关资源
      最近更新 更多