这是 Knuth 乘法哈希的正确实现吗?
int hash(int v)
{
v *= 2654435761;
return v >> 32;
}
乘法中的溢出会影响算法吗?
如何提高这种方法的性能?
Knuth 乘法散列用于{0, 1, 2, ..., 2^p - 1}
从整数 k 计算散列值。
假设它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 < 2^32
保留 n1 的最高 p 位:
n1 = m1 * 2^(32-p) + m2 0 <= m2 < 2^(32-p)
因此,在 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_map
or一起使用std::unordered_set
。但我认为那些哈希表使用素数作为大小,所以 Knuth 乘法哈希在这种情况下没有用。使用hash(x) = x
将非常适合这些表。
资料来源:“算法简介,第三版”,Cormen 等人,13.3.2 p:263
资料来源:“计算机编程艺术,第 3 卷,排序和搜索”,DE Knuth,6.4 p:516
好的,我在 TAOCP 第 3 卷(第 2 版)第 6.4 节第 516 页中查到了它。
这个实现是不正确的,尽管正如我在评论中提到的那样,它可能会给出正确的结果。
一个正确的方法(我认为 - 随意阅读TAOCP的相关章节并验证这一点)是这样的:(重要:是的,你必须将结果右移以减少它,而不是使用按位AND。但是,那不是此功能的责任- 范围缩小不是散列本身的适当部分)
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 = 2 32和 M = 2 32。计算 AK/w 给出了格式为 Q32.32 的定点结果,模 1 步骤仅采用 32 个小数位。但这与做模乘然后说结果是分数位是一样的。当然,当乘以 M 时,由于 M 的选择方式,所有小数位都变为整数位,因此它简化为一个普通的旧模乘法。如前所述,当 M 是 2 的较低幂时,这恰好使结果右移。
可能会迟到,但这是 Knuth 方法的 Java 实现:
对于大小为 N 的哈希表:
public long hash(int key) {
long l = 2654435769L;
return (key * l >> 32) % N ;
}
如果输入参数是一个指针,那么我使用它
#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 实现、字典、集合等中使用它作为默认的后备散列函数......