有哪些整数哈希函数接受一个整数哈希键且表现良好?
我发现以下算法提供了非常好的统计分布。每个输入位对每个输出位的影响概率约为50%。没有碰撞(每个输入都会产生不同的输出)。该算法很快,除非CPU没有内置整数乘法单元。C代码,假设int
为32位(对于Java,请将>>
替换为>>>
并删除unsigned
):
unsigned int hash(unsigned int x) {
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = ((x >> 16) ^ x) * 0x45d9f3b;
x = (x >> 16) ^ x;
return x;
}
如果你将 0x45d9f3b
替换为其乘法逆元0x119de1f3
,就可以反向过程(从哈希获取输入值):
unsigned int unhash(unsigned int x) {
x = ((x >> 16) ^ x) * 0x119de1f3;
x = ((x >> 16) ^ x) * 0x119de1f3;
x = (x >> 16) ^ x;
return x;
}
uint64_t hash(uint64_t x) {
x = (x ^ (x >> 30)) * UINT64_C(0xbf58476d1ce4e5b9);
x = (x ^ (x >> 27)) * UINT64_C(0x94d049bb133111eb);
x = x ^ (x >> 31);
return x;
}
uint64_t unhash(uint64_t x) {
x = (x ^ (x >> 31) ^ (x >> 62)) * UINT64_C(0x319642b2d24d8ec3);
x = (x ^ (x >> 27) ^ (x >> 54)) * UINT64_C(0x96de1b173f119089);
x = x ^ (x >> 30) ^ (x >> 60);
return x;
}
long
,在常量后添加L
,将>>
替换为>>>
并删除unsigned
。x = ((x >> 32) ^ x)
,然后再使用上面的32位乘法。我不确定哪个更好。您可能还想查看Murmur3的64位终结器。 - Thomas Mueller0x45d9f3b
替换为其倒数值 0x119de1f3
即可。 - Thomas MuellerKnuth的乘法哈希方法:
hash(i)=i*2654435761 mod 2^32
一般来说,你应该选择一个乘数,其值与您的哈希大小(在此示例中为2^32
)同阶,并且与其没有公共因数。这样,哈希函数可以均匀地覆盖整个哈希空间。
编辑:这个哈希函数最大的缺点是保留了可除性,因此如果您的整数都能被2或4整除(这种情况很常见),那么它们的哈希也会被整除。这在哈希表中会导致问题-你可能只使用了1/2或1/4的桶。
取决于你的数据分布方式。对于一个简单的计数器,最简单的函数
f(i) = i
会不错(我觉得可能是最优的,但我无法证明)。
快速且良好的哈希函数可以由具有较低质量的快速置换组成,例如
为了产生具有优越品质的哈希函数,就像PCG用于随机数生成一样。
实际上,这也是rrxmrrxmsx_0和murmur哈希使用的配方,无论是有意还是无意地。
我个人发现
uint64_t xorshift(const uint64_t& n,int i){
return n^(n>>i);
}
uint64_t hash(const uint64_t& n){
uint64_t p = 0x5555555555555555ull; // pattern of alternating 0 and 1
uint64_t c = 17316035218449499591ull;// random uneven integer constant;
return c*xorshift(p*xorshift(n,32),32);
}
要足够好。
一个好的哈希函数应该:
让我们首先看一下恒等函数。它满足1.但不满足2. :
输入的第n位决定输出的第n位,与其他位没有关联(蓝色),因此形成了完美的红线(相关性为100%)。
xorshift(n,32)并不比前者更好,只能得到一条半的线。但仍满足1.,因为可以通过再次应用来进行反演。
使用无符号整数进行乘法运算("Knuth's multiplicative method")更好,级联效果更强,并以0.5的概率翻转更多输出位,这正是您想要的,在绿色方面。它满足以下条件:对于每个奇数,都存在一个乘法逆元素。 将两者结合起来,得到以下输出,仍然满足1.,因为两个双射函数的组合产生另一个双射函数。再次使用乘法和xorshift将产生以下结果:
或者你可以使用伽罗瓦域乘法,例如GHash,它们在现代CPU上已经变得相当快,并且具有一步中更优秀的品质。
uint64_t const inline gfmul(const uint64_t& i,const uint64_t& j){
__m128i I{};I[0]^=i;
__m128i J{};J[0]^=j;
__m128i M{};M[0]^=0xb000000000000000ull;
__m128i X = _mm_clmulepi64_si128(I,J,0);
__m128i A = _mm_clmulepi64_si128(X,M,0);
__m128i B = _mm_clmulepi64_si128(A,M,0);
return A[0]^A[1]^B[1]^X[0]^X[1];
}
__m128i I = i; //set the lower 64 bits
,但我不能这样做,所以我使用了 ^=
。0^1 = 1
,因此没有不涉及。关于使用 {}
进行初始化,我的编译器从未抱怨过,这可能不是最好的解决方案,但我想要的是将其全部初始化为 0,以便我可以执行 ^=
或 |=
。我认为我基于 这篇博客文章 编写了该代码,该博客文章还提供了反演,非常有用 :D - Wolfgang Brehm32-bits multiplicative method (very fast) see @rafal
#define hash32(x) ((x)*2654435761)
#define H_BITS 24 // Hashtable size
#define H_SHIFT (32-H_BITS)
unsigned hashtab[1<<H_BITS]
....
unsigned slot = hash32(x) >> H_SHIFT
32-bits and 64-bits (good distribution) at : MurmurHash
自从我发现Thomas Mueller在这个答案中提到的splitmix64
,我一直在使用它。然而,最近我偶然发现了Pelle Evensen的rrxmrrxmsx_0,它比原始的MurmurHash3 finalizer及其后继者(splitmix64
和其他混合函数)具有更好的统计分布。以下是C代码片段:
#include <stdint.h>
static inline uint64_t ror64(uint64_t v, int r) {
return (v >> r) | (v << (64 - r));
}
uint64_t rrxmrrxmsx_0(uint64_t v) {
v ^= ror64(v, 25) ^ ror64(v, 50);
v *= 0xA24BAED4963EE407UL;
v ^= ror64(v, 24) ^ ror64(v, 49);
v *= 0x9FB21C651E98DF25UL;
return v ^ v >> 28;
}
Pelle 还提供了一个关于 MurmurHash3
最后一步使用的 64 位混合器和最近变体的深入分析, 可以在这里查看。
在Eternally Confuzzled网站上有一些哈希算法的概述。我推荐Bob Jenkins的单次哈希,它可以快速达到雪崩效应,因此可用于高效的哈希表查找。
#define MCR_HashTableSize 2^10
unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
key = key*2654435761 & (MCR_HashTableSize - 1)
return key;
}
unsigned int Hash_UInt_M3(unsigned int key)
{
key ^= (key << 13);
key ^= (key >> 17);
key ^= (key << 5);
return key;
}
我认为在不事先了解数据和使用情况的情况下,无法说一个哈希函数是“好”的。对于未知的数据大小,有比哈希表更好的数据结构(假设你正在为哈希表做哈希)。个人而言,当我知道需要在有限的内存中存储“有限”的元素时,我会使用哈希表。在开始考虑哈希函数之前,我会尝试对我的数据进行快速的统计分析,看看数据如何分布等。
hash(x) = x
? - theonlygusti