什么整数散列函数是好的,接受整数散列键?

Lea*_*ear 92 c algorithm hash

什么整数散列函数是好的,接受整数散列键?

Tho*_*ler 133

我发现以下算法提供了非常好的统计分布.每个输入位以大约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;
}
Run Code Online (Sandbox Code Playgroud)

幻数是使用一个运行了几个小时的特殊多线程测试程序计算出来的,该程序计算雪崩效应(如果单个输入位发生变化,输出位数会发生变化;平均应该接近16),独立性输出位发生变化(输出位不应相互依赖),以及每个输出位发生变化的概率(如果有任何输入位发生变化).计算值优于MurmurHash使用的32位终结器,并且几乎与使用AES时一样好(不完全).一个小优点是两次使用相同的常数(它确实使我上次测试时的速度略快,不确定是否仍然如此).

如果替换0x45d9f3bwith 0x119de1f3(乘法逆),则可以反转该过程(从散列中获取输入值):

unsigned int unhash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = ((x >> 16) ^ x) * 0x119de1f3;
    x = (x >> 16) ^ x;
    return x;
}
Run Code Online (Sandbox Code Playgroud)

对于64位数字,我建议使用以下内容,即使它可能不是最快的.这个基于splitmix64,它似乎基于博客文章Better Bit Mixing(mix 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;
}
Run Code Online (Sandbox Code Playgroud)

对于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;
}
Run Code Online (Sandbox Code Playgroud)

更新:您可能还想查看Hash Function Prospector项目,其中列出了其他(可能更好的)常量.

  • 不,这不是一个错字,第二行进一步混合位.仅使用一次乘法就不那么好了. (3认同)
  • 我更改了幻数,因为根据我编写的[测试用例](http://code.google.com/p/h2database/source/browse/trunk/h2/src/test/org/h2/test/store/ CalculateHashConstant.java)值0x45d9f3b提供更好的[混淆和扩散](http://en.wikipedia.org/wiki/Confusion_and_diffusion),特别是如果一个输出位发生变化,则每个其他输出位的变化概率大致相同(在如果输入位改变,则对所有输出位的加法以相同的概率改变).你是如何衡量0x3335b369对你有用的?你是一个int 32位吗? (3认同)
  • 我正在寻找一个很好的哈希函数64位无符号int到32位unsigned int.对于那种情况,上面的幻数会是一样的吗?我移动了32位而不是16位. (3认同)
  • 我相信在这种情况下,一个更大的因素会更好,但你需要进行一些测试.或者(这是我做的)首先使用`x =((x >> 32)^ x)`然后使用上面的32位乘法.我不确定什么更好.您可能还想查看[Murmur3的64位终结器](http://code.google.com/p/smhasher/wiki/MurmurHash3) (3认同)
  • 前两行完全一样!这里有错字吗? (2认同)
  • @RocketRoy如果你之后使用modulo,当然在某些时候会有碰撞.关键不是要避免碰撞,而是要使它们不太可能.你可能获得的最好的概率与真正的随机数相同(不是伪随机数,而是真随机数),但这是不可行的.第二好的是与加密安全随机数发生器(例如AES或SHA-3)相同的概率,但这很慢.上面的算法很快但在概率方面接近AES. (2认同)
  • @SantoshGhimire是的,在这种情况下可以反转操作,因为没有碰撞。您只需要在公式(倒数)中将“ 0x45d9f3b”替换为“ 0x119de1f3”即可。 (2认同)
  • @SantoshGhimire,关键的警告是没有碰撞,否则两个(或更多)不同的值由相同的键表示,其中一个必须是不可逆的.除非内存非常重要,特别是如果是CPU,你不会想要这样做.由于哈希是CPU密集型的,因此最好查找它.散列函数执行范围缩减,最好将其视为有损压缩.请谨慎使用逆转. (2认同)
  • @SantoshGhimire它适用于我,请参阅http://codepad.org/WwSVNRnX上的示例代码 (2认同)

Raf*_*ird 41

Knuth的乘法方法:

hash(i)=i*2654435761 mod 2^32
Run Code Online (Sandbox Code Playgroud)

通常,您应该选择一个乘以您的散列大小(2^32在示例中)的乘数,并且没有与之相关的公因子.这样,哈希函数统一覆盖了所有哈希空间.

编辑:这个哈希函数的最大缺点是它保留了可分性,所以如果你的整数都可以被2或4整除(这并不罕见),它们的哈希也是如此.这是哈希表中的一个问题 - 您最终只能使用1/2或1/4的桶.

  • 这是一个非常糟糕的哈希函数,虽然附有一个着名的名字. (30认同)
  • 对于好奇,这个常量被选择为散列大小(2 ^ 32)除以Phi (10认同)
  • 仔细观察,事实证明2654435761实际上是一个素数.所以这可能是为什么它被选中而不是2654435769. (9认同)
  • Paolo:Knuth的方法是"糟糕的",因为它不会在高位上雪崩 (7认同)
  • 请停止将此哈希函数命名为“Knuth 的乘法方法”。这不是 Knuth 提出的。 (4认同)
  • 如果与主表大小一起使用,它根本不是一个糟糕的哈希函数.此外,它适用于_closed_散列.如果散列值不是均匀分布的,则乘法散列可确保来自一个值的冲突不太可能"干扰"具有其他散列值的项目. (3认同)
  • 值得注意的是,此函数具有非常差的雪崩属性(它以非均匀的方式传播值),因此在用于示例哈希表时可能会导致聚类 (3认同)
  • 答案没有提到必须使用哈希值的“高位”以获得良好的性能(表大小为2的幂的右移)。使用`hash(i)%table_size`会导致大量的冲突。 (2认同)

eri*_*len 26

取决于您的数据如何分布.对于一个简单的计数器,最简单的功能

f(i) = i
Run Code Online (Sandbox Code Playgroud)

会很好(我怀疑是最佳的,但我无法证明).

  • @Rafal:这就是为什么响应说"对于一个简单的计数器"和"取决于你的数据如何分配" (7认同)
  • 由于其分布属性(或缺乏属性),身份函数在许多实际应用中作为哈希是相当无用的,除非当然,局部性是所需的属性 (7认同)
  • 这实际上是Sun在java.lang.Integer中实现方法hashCode()http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b14/java/lang/ Integer.java#Integer.hashCode%28%29 (5认同)
  • @JuandeCarrion这是误导,因为这不是正在使用的哈希.在转换为使用两种表大小的功能之后,Java重新使用`.hashCode()`返回的每个哈希,请参阅[here](http://grepcode.com/file/repository.grepcode.com/java/root/jdk /openjdk/6-b14/java/util/HashMap.java#HashMap.hash%28int%29). (5认同)
  • 这样做的问题在于,通常有大的整数集可以被公共因子整除(字对齐的存储器地址等).现在,如果您的哈希表碰巧可以被相同的因子整除,那么最终只使用了一半(或1/4,1/8等)桶. (3认同)

Lyk*_*kos 16

快速和良好的散列函数可以由质量较差的快速排列组合而成,例如

  • 与一个奇数相乘
  • 二元旋转
  • 异或移位

产生具有优良品质的散列函数,就像用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);
}
Run Code Online (Sandbox Code Playgroud)

要足够好。

一个好的散列函数应该

  1. 如果可能的话,是双射的,不会丢失信息,并且冲突最少
  2. 级联尽可能多且尽可能均匀,即每个输入位应该以 0.5 的概率翻转每个输出位。

我们先来看看恒等函数。它满足 1. 但不满足 2. :

身份函数

输入位 n 确定输出位 n 的相关性为 100%(红色),没有其他相关性,因此它们是蓝色的,给出了一条完美的红线。

xorshift(n,32) 也好不到哪里去,只产生一条半线。仍然满足 1.,因为它与第二个应用程序是可逆的。

异或移位

与无符号整数的乘法“Knuth 的乘法方法”)要好得多,级联更强烈,并以 0.5 的概率翻转更多输出位,这是您想要的,绿色。满足1。对于每个奇数,都有一个乘法逆。

克努斯

将两者结合给出以下输出,仍然满足 1. 因为两个双射函数的组合产生另一个双射函数。

knuth•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];                                              
   }
Run Code Online (Sandbox Code Playgroud)

  • @VioletGiraffe如果哈希不是双射的,则意味着输出分布不能均匀。如果哈希是双射的,则意味着它使用尽可能多的输出空间。这就是为什么建议哈希是双射的,使其可逆,但只忽略计算可行性。 (2认同)

Tyl*_*nry 7

这个页面列出了一些简单的散列函数,这些散列函数通常都很合适,但是任何简单的散列都有病态的情况,它不能正常工作.


bil*_*ill 5


Fre*_*ong 5

自从我发现这个线程以来,我一直在使用splitmix64(在 Thomas Mueller 的回答中指出)。然而,我最近偶然发现了 Pelle Evensen 的rrxmrrxmsx_0,它产生了比原始 MurmurHash3 终结器及其后继者(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;
}
Run Code Online (Sandbox Code Playgroud)

Pelle 还对最后一步中使用的 64 位混音器以及最新变体进行了深入分析。MurmurHash3

  • 该函数不是双射的。对于所有 v,其中 v = ror(v,25),即全 0 和全 1,它将在两个地方产生相同的输出。对于所有值 v = ror64(v, 24) ^ ror64(v, 49),它们至少多了两个并且与 v = ror(v,28) 相同,产生另一个 2^4 ,总共大约 22 次不必要的碰撞。splitmix 的两个应用程序可能同样好、同样快,但仍然可逆且无碰撞。 (3认同)