这是Knuth乘法散列的正确实现。
int hash(int v)
{
v *= 2654435761;
return v >> 32;
}
乘法中的溢出会影响算法吗?
如何提高此方法的性能?
答案 0 :(得分:13)
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 <1。 2 ^ 32
保持n1的最高p位:
n1 = m1 * 2 ^(32-p)+ m2,其中0 <= m2 <1。 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,他从未说过这样的话。看起来有些人认为他是聪明的&#34;决定采用接近2 654 435 769的素数。
请记住,大多数哈希表实现都不允许在其界面中使用此类签名,因为它们只允许
uint32_t hash(int x);
并减少hash(x)
modulo 2 ^ p来计算x的哈希值。那些哈希表不能接受Knuth乘法哈希。这可能是为什么这么多人忘记采用更高的p位完全破坏算法的原因。
因此,您无法使用std::unordered_map
或std::unordered_set
的Knuth乘法哈希值。但我认为这些哈希表使用素数作为大小,因此Knuth乘法哈希在这种情况下没有用。使用hash(x) = x
非常适合这些表格。
资料来源:&#34;算法导论,第三版&#34;,Cormen等,13.3.2 p:263
资料来源:&#34;计算机编程艺术,第3卷,排序和搜索&#34;,D.E。 Knuth,6.4 p:516
答案 1 :(得分:11)
好的,我在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
(而不是int
) - 它们确保乘法溢出模2 ^ 32,因为如果选择32作为字大小,它应该会这样做。这里也没有k
的右移,因为没有理由将范围缩减归功于基本散列函数,实际上获得完整结果更有用。常量2654435761来自问题,实际建议的常量是2654435769,但这是一个很小的差异,据我所知,不会影响哈希的质量。
其他有效的实现将结果右移一定量(不是完整的字大小,这没有意义,C ++不喜欢它),具体取决于你需要多少位哈希。或者他们可以使用其他常数(受某些条件限制)或其他字数。减少散列模数是不一个有效的实现,但是一个常见的错误,可能它是在散列上进行范围缩减的事实上的标准方法。乘法散列的底部位是最差质量的位(它们依赖于较少的输入),如果您确实需要更多位,则只想使用它们,而减少散列模2的幂将返回只有最差的位。实际上,这相当于丢弃了大部分输入位。减少模2的非幂次并不是那么糟糕,因为它确实混合了更高的位,但不是如何定义乘法散列。
类型应该是无符号的,否则溢出是未指定的(因此可能是错误的,不仅在非二进制补码架构上,而且在过于聪明的编译器上),可选的右移是一个有符号的移位(错误)。 / p>
在我在顶部提到的页面上,有这个公式:
这里我们有A = 2654435761(或2654435769),w = 2 32 且M = 2 32 。计算AK / w得到格式为Q32.32的定点结果,mod 1步只得到32分数位。但这与进行模乘,然后说结果是分数位是一回事。当然,当乘以M时,由于如何选择M,所有分数位都变为整数位,因此它简化为仅仅是一个普通的模乘法。当M是2的较低幂时,正好如上所述右移结果。
答案 2 :(得分:0)
如果输入参数是一个指针,那么我使用这个
#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实现,字典,集合等中的默认回退哈希函数...
答案 3 :(得分:0)
可能会迟到,但这是Knuth方法的Java实现:
对于大小为N的哈希表:
public long hash(int key) {
long l = 2654435769L;
return (key * l >> 32) % N ;
}