【问题标题】:Analysis of the usage of prime numbers in hash functions哈希函数中素数的使用分析
【发布时间】:2011-03-07 20:24:19
【问题描述】:

我正在研究基于散列的排序,我发现在散列函数中使用素数被认为是一个好主意,因为将键的每个字符乘以素数并将结果相加会产生一个唯一值(因为素数是唯一的),像 31 这样的素数会产生更好的键分布。

key(s)=s[0]*31(len–1)+s[1]*31(len–2)+ ... +s[len–1]

示例代码:

public int hashCode( ) 
{
    int h = hash;
    if (h == 0) 
    {
        for (int i = 0; i < chars.length; i++) 
        {
            h = MULT*h + chars[i];
        }
        hash = h;
    }
    return h;
}

我想了解为什么在下面的解释中使用偶数乘以每个字符是一个坏主意(在另一个论坛上找到;这听起来像是一个很好的解释,但我没能理解它)。如果以下推理不成立,我将不胜感激。

假设 MULT 为 26,并考虑 散列一百个字符的字符串。 弦的影响有多大 第一个角色在决赛中 “h”的值?第一个字符的值 将乘以 MULT 99 次,所以如果算术完成 在无限精度下,该值将 由一些混乱的位组成 后跟 99 个低位零位—— 每次乘以 MULT 你 引入另一个低阶零, 正确的?计算机是有限的 算术只是砍掉所有 多余的高阶位,所以第一个 角色对“h”的实际贡献 是……正好为零! “h”值 仅取决于最右边的 32 字符串字符(假设为 32 位 int),即使这样,事情也不是 精彩:最后 32 个中的第一个 字节只影响最左边的位 'h' 并且对 剩下 31 个。显然,一个偶数 MULT 是个坏主意。

【问题讨论】:

  • 你应该使用unsigned int,C 中的整数溢出是未定义的行为(任何事情都可能发生)。

标签: c++ hashtable hash


【解决方案1】:

我认为使用 2 而不是 26 更容易看出。它们对h 的最低位具有相同的效果。考虑一个由 33 个字符组成的字符串 c,后跟 32 个零字节(用于说明目的)。由于字符串并非完全为空,因此您希望哈希值不为零。

对于第一个字符,您计算的哈希值h 等于c[0]。对于第二个字符,你取 h * 2 + c[1]。所以现在h2*c[0]。对于第三个字符h 现在是h*2 + c[2],它的结果是4*c[0]。再重复 30 次,您可以看到乘法器使用的位数比您的目的地可用的位数多,这意味着 c[0] 对最终哈希完全没有影响。

使用不同的乘数(例如 26),最终的数学运算结果完全相同,只是中间哈希值在此过程中会每隔一段时间对 2^32 取模。由于 26 是偶数,它仍然在每次迭代的低端添加一个 0 位。

【讨论】:

  • “再重复 30 次,您会看到乘法器使用的位数比您的目的地可用的位数多,这意味着实际上 c[0] 对最终哈希完全没有影响”..你能解释一下吗?谢谢!
【解决方案2】:

这个散列可以这样描述(这里的^是取幂,不是异或)。

hash(string) = sum_over_i(s[i] * MULT^(strlen(s) - i - 1)) % (2^32).

看第一个字符的贡献。这是

(s[0] * MULT^(strlen(s) - 1)) % (2^32).

如果字符串足够长(strlen(s) > 32),那么这个值为零。

【讨论】:

  • "如果字符串足够长 (strlen(s) > 32) 那么这是零"...你能解释一下吗?一点插图会有所帮助...谢谢!
【解决方案3】:

其他人已经发布了答案——如果你使用偶数倍数,那么只有字符串中的最后一个字符对计算散列很重要,因为早期字符的影响将移出寄存器。

现在让我们考虑一下当你使用像 31 这样的乘数时会发生什么。嗯,31 是 32-1 或 2^5 - 1。所以当你使用它时,你的最终哈希值将是:

\sum{c_i 2^{5(len-i)} - \sum{c_i}

不幸的是,stackoverflow 不理解 TeX 数学符号,所以上面的内容很难理解,但它对字符串中的字符进行了两次求和,其中第一个对字符串中的每个后续字符将每个字符移动 5 位。因此,使用 32 位机器时,除了字符串的最后七个字符之外,所有字符都会从顶部移出。

这样做的结果是,使用 31 的乘数意味着虽然最后七个以外的字符对字符串有影响,但它完全独立于它们的顺序。如果您取两个具有相同最后 7 个字符的字符串,而其他字符也相同但顺序不同,您将获得相同的哈希值。除了最后 7 个字符之外,您还将获得像“az”和“by”这样的相同哈希值。

因此,使用素数乘数虽然比偶数乘数好得多,但仍然不是很好。更好的是使用旋转指令,当它们移出顶部时,它将位移回底部。比如:

public unisgned hashCode(string chars)
{
    unsigned h = 0;
    for (int i = 0; i < chars.length; i++) {
        h = (h<<5) + (h>>27);  // ROL by 5, assuming 32 bits here
        h += chars[i];
    }
    return h;
}

当然,这取决于您的编译器是否足够聪明,能够识别旋转指令的习语并将其转换为单条指令以获得最大效率。

这仍然存在一个问题,即在字符串中交换 32 个字符的块会给出相同的哈希值,因此它远非强大,但可能足以满足大多数非加密目的

【讨论】:

    【解决方案4】:

    会产生独特的价值

    停在那里。哈希不是唯一的。一个好的散列算法可以最大限度地减少冲突,但鸽巢原理向我们保证,完全避免冲突是不可能的(对于任何具有重要信息内容的数据类型)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-23
      • 2012-08-19
      • 1970-01-01
      相关资源
      最近更新 更多