【发布时间】:2010-10-14 10:07:59
【问题描述】:
接受整数散列键的整数散列函数是好的?
【问题讨论】:
接受整数散列键的整数散列函数是好的?
【问题讨论】:
我发现以下算法提供了非常好的统计分布。每个输入位以大约 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;
}
使用运行了多个小时的special multi-threaded test program 计算了幻数,它计算了雪崩效应(如果单个输入位发生变化,输出位的数量会发生变化;平均应该接近 16),独立于输出位变化(输出位不应相互依赖),以及任何输入位发生变化时每个输出位发生变化的概率。计算的值优于MurmurHash 使用的 32 位终结器,并且几乎与使用 AES 时一样好(不完全)。一个轻微的优势是相同的常量被使用了两次(它确实使它在我上次测试时稍微快了一点,不确定是否仍然如此)。
如果将0x45d9f3b 替换为0x119de1f3(multiplicative inverse),则可以反转该过程(从哈希中获取输入值):
unsigned int unhash(unsigned int x) {
x = ((x >> 16) ^ x) * 0x119de1f3;
x = ((x >> 16) ^ x) * 0x119de1f3;
x = (x >> 16) ^ x;
return x;
}
对于 64 位数字,我建议使用以下,即使它可能不是最快的。这个是基于splitmix64的,貌似是基于博客文章Better Bit Mixing(混13)。
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;
}
对于 Java,使用 long,将 L 添加到常量中,将 >> 替换为 >>> 并删除 unsigned。在这种情况下,倒车就更复杂了:
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;
}
更新:您可能还想查看Hash Function Prospector 项目,其中列出了其他(可能更好的)常量。
【讨论】:
x = ((x >> 32) ^ x),然后使用上面的 32 位乘法。我不确定什么更好。您可能还想看看the 64-bit finalizer for Murmur3
Knuth 的乘法方法:
hash(i)=i*2654435761 mod 2^32
一般来说,您应该选择一个与您的哈希大小(示例中为2^32)并没有公因数的乘数。这样哈希函数就可以统一覆盖所有的哈希空间。
编辑:这个散列函数的最大缺点是它保留了可分性,所以如果你的整数都可以被 2 或 4 整除(这并不罕见),它们的散列值也可以。这是哈希表中的一个问题 - 您最终只能使用 1/2 或 1/4 的存储桶。
【讨论】:
取决于您的数据的分布方式。对于一个简单的计数器,最简单的函数
f(i) = i
会很好(我怀疑是最优的,但我无法证明)。
【讨论】:
.hashCode() 返回的每个散列,请参阅here。
快速且良好的哈希函数可以由质量较差的快速排列组合而成,例如
产生具有卓越品质的散列函数,例如用PCG 演示的随机数生成。
这实际上也是 rrxmrrxmsx_0 和 murmur hash 有意或无意地使用的配方。
我个人发现
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。对于每个不均匀的整数都有一个乘法逆元。
将两者结合得到以下输出,仍然满足 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,这样我就可以做到 ^= 或 |= 。我想我基于this blogpost 的代码,它也给出了反转,非常有用:D
32 位乘法方法(非常快)见 @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 位和 64 位(良好分布)在:MurmurHash
【讨论】:
This page 列出了一些简单的散列函数,这些函数总体上趋于正常,但是任何简单的散列都有病态的情况,它不能很好地工作。
【讨论】:
Eternally Confuzzled 对一些哈希算法进行了很好的概述。我推荐 Bob Jenkins 的一次一个哈希,它很快就会达到雪崩,因此可以用于高效的哈希表查找。
【讨论】:
【讨论】:
自从我找到这个帖子以来,我一直在使用 splitmix64(指向 Thomas Mueller 的 answer)。然而,我最近偶然发现了 Pelle Evensen 的 rrxmrrxmsx_0,它产生了比最初的 MurmurHash3 终结器及其后继者(splitmix64 和其他混合)更好的统计分布。这是C语言中的sn-p代码:
#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 还提供in-depth analysis 的MurmurHash3 的最后一步中使用的 64 位混音器以及更新的变体。
【讨论】:
我认为我们不能在事先不知道您的数据的情况下说散列函数是“好”的!并且不知道你将如何处理它。
对于未知数据大小,有比散列表更好的数据结构(我假设您在这里为散列表进行散列)。当我知道我有“有限”数量的元素需要存储在有限的内存中时,我会亲自使用哈希表。在我开始考虑我的哈希函数之前,我会尝试对我的数据进行快速统计分析,看看它是如何分布的等。
【讨论】:
对于随机hash值,有工程师说黄金比例素数(2654435761)是一个不好的选择,我的测试结果发现不是真的;相反,2654435761 可以很好地分配哈希值。
#define MCR_HashTableSize 2^10
unsigned int
Hash_UInt_GRPrimeNumber(unsigned int key)
{
key = key*2654435761 & (MCR_HashTableSize - 1)
return key;
}
哈希表大小必须是 2 的幂。
我编写了一个测试程序来评估整数的许多哈希函数,结果表明 GRPrimeNumber 是一个不错的选择。
我试过了:
根据我的测试结果,我发现黄金比例素数总是有更少的空桶或零空桶和最短的碰撞链长度。
一些整数的hash函数号称是好的,但测试结果表明,当total_data_entry / total_bucket_number = 3时,最长链长度大于10(最大碰撞数> 10),很多桶没有映射(空桶),与黄金比例素数散列的零空桶和最长链长度3的结果相比,这是非常糟糕的。
顺便说一句,根据我的测试结果,我发现一个版本的移位异或哈希函数非常好(由 mikera 共享)。
unsigned int Hash_UInt_M3(unsigned int key)
{
key ^= (key << 13);
key ^= (key >> 17);
key ^= (key << 5);
return key;
}
【讨论】: