【问题标题】:knuth multiplicative hashknuth 乘法哈希
【发布时间】:2012-08-05 23:02:37
【问题描述】:

这是 Knuth 乘法哈希的正确实现吗?

int hash(int v)
{
    v *= 2654435761;
    return v >> 32;
}

乘法中的溢出会影响算法吗?

如何提高这种方法的性能?

【问题讨论】:

  • 你几乎肯定想使用unsigned int(或unsigned long long,因为它似乎取决于>32位的大小)而不是普通的int
  • 是的,溢出肯定会阻止这个工作。事实上,如果您的 int 是典型的,则此代码将始终返回 0 或 -1。
  • 你的位移是(如果 int 是 32 位)到远。你的哈希应该有多少位?从 32 中减去它们
  • @Fox32 好的,所以如果我想要 32 位哈希,我不需要移位
  • 假设典型情况下,溢出实际上是可以的(虽然不是可移植的)。移位很奇怪(尝试丢弃所有位),但意外且不可移植通常可以正常工作(对于 x86 上的 32 位参数,移位量取模 32)。

标签: c++ algorithm hash


【解决方案1】:

Knuth 乘法散列用于从整数 k 计算 {0, 1, 2, ..., 2^p - 1} 中的散列值。

假设p在0到32之间,算法如下:

  • 将 alpha 计算为最接近 2^32 (-1 + sqrt(5)) / 2 的整数。我们得到 alpha = 2 654 435 769。

  • 计算 k * alpha 并以 2^32 为模减少结果:

    k * alpha = n0 * 2^32 + n1 与 0

  • 保留n1的最高p位:

    n1 = m1 * 2^(32-p) + m2 0

所以,在 C++ 中 Knuth 乘法算法的正确实现是:

std::uint32_t knuth(int x, int p) {
    assert(p >= 0 && p <= 32);

    const std::uint32_t knuth = 2654435769;
    const std::uint32_t y = x;
    return (y * knuth) >> (32 - p);
}

忘记将结果移动 (32 - p) 是一个重大错误。因为您将失去哈希的所有良好属性。它将偶数序列转换为偶数序列,这将非常糟糕,因为所有奇数槽都将保持空置状态。这就像拿一杯好酒和可乐混合。顺便说一句,网络上到处都是错误引用 Knuth 并使用乘以 2 654 435 761 而没有取高位的人。我刚打开 Knuth,他从来没有说过这样的话。看起来某个自认为“聪明”的人决定取一个接近 2 654 435 769 的素数。

请记住,大多数哈希表实现不允许在其接口中使用这种签名,因为它们只允许

uint32_t hash(int x);

并减少 hash(x) 模 2^p 以计算 x 的哈希值。那些哈希表不能接受 Knuth 乘法哈希。这可能是为什么这么多人因为忘记取更高的 p 位而完全破坏了算法的原因。 因此,您不能将 Knuth 乘法哈希与 std::unordered_mapstd::unordered_set 一起使用。但我认为那些哈希表使用素数作为大小,因此 Knuth 乘法哈希在这种情况下没有用。使用hash(x) = x 将非常适合这些表。

来源:“算法简介,第三版”,Cormen 等人,13.3.2 p:263

来源:“计算机编程艺术,第 3 卷,排序和搜索”,D.E.克努斯,6.4 页:516

【讨论】:

  • 人们“忘记取高 p 位”的主要原因难道不是因为他们使用的是 4 位无符号整数,p = 32,所以32 - p 是 0?
  • 另请注意,如果使用完整的 32 位,哈希会将偶数转换为偶数。
【解决方案2】:

好的,我在 TAOCP 第 3 卷(第 2 版)第 6.4 节第 516 页中查到了。

这个实现是不正确的,尽管正如我在 cmets 中提到的那样,它可能无论如何都会给出正确的结果。

正确的方法(我认为 - 随意阅读 TAOCP 的相关章节并验证这一点)是这样的:(重要:是的,您必须将结果右移以减少它,而不是使用按位与。但是,这不是 这个函数的责任 - 范围缩小不是散列本身的适当部分)

uint32_t hash(uint32_t v)
{
    return v * UINT32_C(2654435761);
    // do not comment about the lack of right shift. I'm not ignoring it. read on.
}

注意uint32_t's(与int's 相反)-它们确保乘法溢出模2 ^ 32,如果您选择32作为字长应该这样做。 k 这里也没有右移,因为没有理由将范围缩小的责任交给基本的散列函数,实际上获得完整结果更有用。常量 2654435761 来自问题,实际建议的常量是 2654435769,但据我所知,这是一个很小的差异,不会影响哈希的质量。

其他有效的实现将结果向右移动一些量(虽然不是完整的字长,这没有意义,C++ 也不喜欢它),具体取决于您需要多少位散列。或者他们可以使用其他常量(受某些条件限制)或其他字长。减少哈希模数不是一个有效的实现,而是一个常见的错误,可能是对哈希进行范围减少的事实上的标准方法。乘法哈希的底部位是质量最差的位(它们依赖于较少的输入),如果您确实需要更多位,您只想使用它们,同时减少哈希模 2 的幂将返回 只有最差的位。事实上,这也相当于丢弃了大部分输入位。以非二次方为模减少并不是那么糟糕,因为它确实混合了较高的位,但这不是乘法哈希的定义方式。

所以要清楚,是的,有一个右移,但那是 范围缩小 而不是 散列 并且只能是哈希表的责任,因为它取决于关于它的内部尺寸。

类型应该是无符号的,否则溢出是未指定的(因此可能是错误的,不仅在非 2 的补码架构上而且在过于聪明的编译器上也是如此)并且可选的右移将是有符号的移位(错误)。

在我在顶部提到的页面上,有这个公式:

这里我们有 A = 2654435761(或 2654435769),w = 232 和 M = 232。计算 AK/w 给出了格式为 Q32.32 的定点结果,模 1 步骤仅采用 32 个小数位。但这与做模乘然后说结果是分数位是一样的。当然,当乘以 M 时,由于 M 的选择方式,所有小数位都变为整数位,因此它简化为一个普通的旧模乘法。如前所述,当 M 是 2 的低次方时,结果会右移。

【讨论】:

  • "CS 3110 Lecture 21: Hash functions: Multiplicative hashing" 声称“除以 2^q 至关重要。进行乘法哈希时的常见错误是忘记这样做”。
  • @DavidCary 这很好,我在这里所做的是选择 M=2^32,但是当并非所有 32 位都需要时,必须丢弃底部位。
  • @harold 这个维基百科条目en.wikipedia.org/wiki/Hash_table#Choosing_a_good_hash_function 声称乘法散列的聚类能力很差,因此不适合开放寻址方案(en.wikipedia.org/wiki/Open_addressing)。它有多真实?如果是这种情况,是否有替代方案?非常感谢
  • @bytefire 根据我的经验,这还不错。请注意,该声明的来源使用了底部位,这是最糟糕的事情,所以他们得到一个糟糕的结果也就不足为奇了。一个产品的低位不依赖于输入中的任何高位,因此它相当于在散列之前丢弃了大部分密钥。
  • @harold 很公平。我认为谈论具体的位数会清楚地表明您在谈论已经溢出的乘法结果:“如果您需要将生成的 32 位哈希值减少到一个较小的数字,比如 24 位,因为系统中的某些内容需要 24 位值,那么您应该使用前 24 位(即右移 8 位)而不是使用后 24 位,因为高位取决于更多的输入。”
【解决方案3】:

可能会迟到,但这是 Knuth 方法的 Java 实现:

对于大小为 N 的哈希表:

public long hash(int key) {
    long l = 2654435769L;
    return (key * l >> 32) % N ;
}

【讨论】:

  • 为什么不使用长文本2654435769L?打电话给parseLong这里似乎是奖励
  • 对,应该可以轻松做到这一点:) 已编辑
  • 由于这是 Java,您应该使用 &gt;&gt;&gt; 进行班次。
  • 我认为您实际上并没有运行此代码。这个实现是垃圾,不是 Knuth 的算法。请注意,hash(0) 是 0,hash(1) 是 0,hash(2) 是 1,等等。你几乎把所有的比特都扔掉了。
【解决方案4】:

如果输入参数是一个指针,那么我使用这个

#include <inttypes.h>

uint32_t knuth_mul_hash(void* k) {
  ptrdiff_t v = (ptrdiff_t)k * UINT32_C(2654435761);
  v >>= ((sizeof(ptrdiff_t) - sizeof(uint32_t)) * 8); // Right-shift v by the size difference between a pointer and a 32-bit integer (0 for x86, 32 for x64)
  return (uint32_t)(v & UINT32_MAX);
}

我通常在 hashmap 实现、字典、集合等中使用它作为默认的后备哈希函数...

【讨论】:

    猜你喜欢
    • 2015-04-12
    • 1970-01-01
    • 2018-03-31
    • 1970-01-01
    • 2016-06-23
    • 2019-08-29
    • 1970-01-01
    • 2012-06-03
    • 2015-09-02
    相关资源
    最近更新 更多