什么样的整数哈希函数可以接受整数哈希键?


Answers:


47

Knuth的乘法方法:

hash(i)=i*2654435761 mod 2^32

通常,您应该选择一个按哈希大小顺序排列的乘数(2^32在示例中),并且没有公因子。这样,哈希函数可以均匀地覆盖您的所有哈希空间。

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


36
这是一个非常糟糕的哈希函数,尽管它附加了一个著名的名称。
Seen Osewa 2010年

5
如果与主表大小一起使用,这根本不是一个糟糕的哈希函数。同样,它也用于封闭式哈希。如果哈希值不是均匀分布的,则乘法哈希可确保来自一个值的冲突不太可能使其他哈希值“干扰”项目。
Paolo Bonzini 2011年

11
出于好奇,将此常量选择为哈希大小(2 ^ 32)除以Phi
awdz9nld 2012年

7
Paolo:Knuth的方法“不好”,因为它不会在高位上发生雪崩
awdz9nld

9
经过仔细检查,发现2654435761实际上是质数。因此,这可能就是为什么选择它而不是2654435769的原因。–
karadoc

149

我发现以下算法提供了很好的统计分布。每个输入位以大约50%的概率影响每个输出位。没有冲突(每个输入导致不同的输出)。该算法速度很快,除非CPU没有内置的整数乘法单元。假设int是32位的C代码(对于Java,请替换>>>>>remove unsigned):

unsigned int hash(unsigned int x) {
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = ((x >> 16) ^ x) * 0x45d9f3b;
    x = (x >> 16) ^ x;
    return x;
}

幻数是使用运行了多个小时的特殊多线程测试程序计算得出的,该程序计算雪崩效应(如果更改单个输入位,则输出位的数量会发生变化;平均应该接近16个),输出位发生变化(输出位不应相互依赖),以及任何输入位发生变化时每个输出位发生变化的可能性。计算出的值比MurmurHash使用的32位终结器更好,并且几乎与使用AES时一样好(不太好)。一个轻微的好处是,相同的常量被使用了两次(上次测试时确实使它稍快一些,不确定是否仍然如此)。

你可以逆转这一过程(您可以通过哈希输入值),如果更换0x45d9f3b0x119de1f3(在乘法逆):

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到常量,替换>>>>>和remove 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项目,其中列出了其他(可能更好)的常量。


2
前两行完全相同!这里有错字吗?
Kshitij Banerjee 2012年

3
不,这不是错字,第二行进一步混合了位。仅使用一个乘法是不好的。
Thomas Mueller 2012年

3
我更改了幻数,因为根据测试用例,我将值0x45d9f3b 为更好的混淆和扩散,特别是如果一个输出位发生变化,则其他输出位发生变化的可能性几乎相同(除了所有输出位都随着变化而变化)。如果输入位发生变化的可能性相同)。您如何衡量0x3335b369更适合您?一个int 32位适合您吗?
Thomas Mueller 2012年

3
我正在寻找一个很好的哈希函数,将64位unsigned int转换为32位unsigned int。是那种情况,上面的魔数会一样吗?我将32位而不是16位移位了。
亚历山德罗2012年

3
我相信在那种情况下,更大的因素会更好,但是您需要运行一些测试。或者(这是我的工作)首先使用x = ((x >> 32) ^ x),然后使用上面的32位乘法。我不确定哪个更好。您可能还想看看Murmur3的64位终结器
Thomas Mueller

29

取决于数据的分布方式。对于一个简单的计数器,最简单的功能

f(i) = i

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


3
这样做的问题是,通常有大的整数集可以被一个公因子(字对齐的内存地址等)整除。现在,如果哈希表恰好可以被相同的因子整除,那么最终只能使用一半(或1 / 4、1 / 8等)存储桶。
2009年

8
@Rafal:这就是为什么响应中显示“用于简单计数器”和“取决于您的数据如何分布”的原因
erikkallen 2009年


5
@JuandeCarrion这具有误导性,因为这不是正在使用的哈希。转移到使用两种表大小的功能后,Java会重新混合从其返回的每个哈希.hashCode(),请参见此处
Esailija

8
身份函数由于其分布特性(或缺乏其属性),在许多实际应用中作为哈希实际上是没有用的,除非当然是局部性是所需的属性
awdz9nld

12

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

  • 与不均​​匀整数相乘
  • 二进制旋转
  • 异或移位

产生具有较高质量的哈希函数,例如用PCG演示的随机数生成方法。

实际上,这也是有意或无意地使用的食谱rrxmrrxmsx_0和杂语哈希。

我个人发现

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. 尽可能多地级联,即每个输入位应以0.5的概率翻转每个输出位。

首先让我们看一下身份功能。它满足1.但不满足2.:

身份功能

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

xorshift(n,32)并不好,只产生一行和一半的行。仍然令人满意,因为它在第二个应用程序中是可逆的。

异或移位

与无符号整数相乘会更好,级联效果更好,并以绿色的概率为0.5(这就是您想要的)翻转更多的输出位。满足1.因为每个不均匀整数都有一个乘法逆。

克努斯

将这两个函数结合在一起,得到的输出仍然满足1.,因为两个双射函数的组合会产生另一个双射函数。

knuth•xorshift

乘法和xorshift的第二个应用将产生以下结果:

建议的哈希

或者,您可以使用诸如GHash之类的Galois字段乘法,它们在现代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];                                              
   }

gfmul:该代码似乎是伪代码,因为afaik不能将括号与__m128i一起使用。还是很有趣的。第一行似乎说“取一个统一的__m128i(I)并用(参数)i对其进行异或运算。我应该将此读为用0初始化I并用i进行异或运算吗?如果这样,它将与用i加载I相同并在I上执行not(操作)?
1

@Jan我想要做的是__m128i I = i; //set the lower 64 bits,但是我做不到,所以我正在使用^=0^1 = 1因此没有参与。关于与初始化{}我的编译器从不抱怨,它可能不是最好的解决办法,但我想这是所有INITIALISE它为0,所以我可以做^=|=。我想我是根据此博客文章上的代码编写的,它也提供了反转功能,非常有用:D
Wolfgang Brehm,

6

本页列出了一些简单的哈希函数,这些函数通常看起来很不错,但是任何简单的哈希都具有无法正常工作的病理情况。


6
  • 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

  • 整数哈希函数

3

Eternally Confuzzled上有一些很好的哈希算法概述。我建议使用鲍勃·詹金斯(Bob Jenkins)的一次性哈希,该哈希可以很快达到雪崩状态,因此可用于高效的哈希表查找。


4
那是一篇很好的文章,但是它专注于哈希字符串键,而不是整数。
Adrian Mouat 2010年

需要明确说明的是,尽管本文中的方法适用于整数(或可以适用于整数),但我认为存在更有效的整数算法。
Adrian Mouat 2010年

2

答案取决于很多事情,例如:

  • 您打算在哪里使用它?
  • 您打算如何处理哈希?
  • 您需要加密安全的哈希函数吗?

我建议您看一下SHA-1等哈希函数的Merkle-Damgard系列


1

我认为如果不事先知道您的数据就不能说哈希函数是“好”的!并且不知道您将如何处理它。

对于未知数据大小,有比散列表更好的数据结构(我假设您在这里对散列表进行散列)。当我知道需要存储在有限数量的内存中的元素数量有限时,我将亲自使用哈希表。在开始考虑哈希函数之前,我将尝试对数据进行快速统计分析,查看其分布情况等。


1

对于随机哈希值,一些工程师说黄金比率素数(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是一个不错的选择。

我努力了:

  1. total_data_entry_number / total_bucket_number = 2、3、4; 其中total_bucket_number =哈希表大小;
  2. 将哈希值域映射到存储桶索引域;也就是说,使用(hash_table_size-1)通过“逻辑与运算”将哈希值转换为存储区索引,如Hash_UInt_GRPrimeNumber()所示;
  3. 计算每个铲斗的碰撞次数;
  4. 记录尚未映射的桶,即一个空桶;
  5. 找出所有铲斗的最大碰撞次数;即最长的链长;

通过测试结果,我发现黄金比率素数始终具有较少的空桶或零空桶,并且碰撞链长度最短。

一些用于整数的哈希函数被认为是好的,但是测试结果表明,当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;
}

2
但是,为什么不正确移动产品,以便保留最混合的位呢?那就是它应该起作用的方式
哈罗德

1
@harold,黄金比例素数是经过精心选择的,尽管我认为这没有什么区别,但我将测试一下“最混合的位”是否更好。我的观点是“这不是一个好选择”。是不正确的,如测试结果所示,仅抓取较低位的部分就足够了,甚至比许多哈希函数还好。
Chen-ChungChia,

(2654435761,4295203489)是素数的黄金比例。
Chen-ChungChia '19

(1640565991,2654435761)也是素数的黄金比例。
陈忠义

@harold,向右移动产品会变得更糟,即使仅向右移动1个位置(除以2),它也会变得更糟(尽管空桶仍然为零,但最长链长较大);向右移动更多位置,结果变得更糟。为什么?我认为原因是:转移产品权利会产生更多的哈希值,而不是互质,只是我的猜测,真正的原因涉及数论。
陈忠义

1

自从找到此线程以来,我一直在使用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;
}

Pelle还提供了对最新版本和最新版本中使用的64位混合器的深入分析MurmurHash3


2
此功能不是双射的。对于所有v,其中v = ror(v,25),即全0和全1,它将在两个位置产生相同的输出。对于所有值v = ror64(v,24)^ ror64(v,49),它们至少至少两个相同且与v = ror(v,28)相同,产生另一个2 ^ 4,总计约22次不必要的碰撞。splitmix的两个应用程序可能一样好,一样快,但是仍然是可逆且无碰撞的。
Wolfgang Brehm
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.