什么整数散列函数适合接受整数散列键?

时间:2009-03-19 20:54:39

标签: c algorithm hash

什么整数散列函数适合接受整数散列键?

11 个答案:

答案 0 :(得分:120)

我发现以下算法提供了非常好的统计分布。每个输入位以大约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替换为0x119de1f3multiplicative 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项目,其中列出了其他(可能更好)的常量。

答案 1 :(得分:38)

Knuth的乘法方法:

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

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

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

答案 2 :(得分:25)

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

f(i) = i

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

答案 3 :(得分:7)

This page列出了一些简单的哈希函数,这些哈希函数通常都是不错的,但是任何简单的哈希都有病态的情况,它不能正常工作。

答案 4 :(得分:6)

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

  • 不规则整数的乘法
  • 二进制旋转
  • xorshift

要产生具有更高质量的哈希函数,就像用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 = 0x5555555555555555; // 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.:

identity function

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

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

xorshift

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

knuth

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

knuth•xorshift

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

proposed hash

或者您可以使用像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];                                              
   }

答案 5 :(得分:5)

  • 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
    
  • 位于MurmurHash的32位和64位(良好分布)

  • Integer Hash Function

答案 6 :(得分:3)

Eternally Confuzzled对一些哈希算法有一个很好的概述。我推荐Bob Jenkins的一次性哈希值,它很快就会达到雪崩,因此可用于高效的哈希表查找。

答案 7 :(得分:2)

答案取决于很多事情:

  • 您打算在哪里使用它?
  • 你想用哈希做什么?
  • 你需要一个密码安全的哈希函数吗?

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

答案 8 :(得分:1)

我不认为我们可以在不事先知道您的数据的情况下说哈希函数是“好”的!并且不知道你将要用它做什么。

对于未知数据大小,有比哈希表更好的数据结构(我假设你在这里为哈希表进行哈希)。当我知道我有一个“有限”数量的元素需要存储在有限的内存中时,我会亲自使用哈希表。在开始考虑我的哈希函数之前,我会尝试对我的数据进行快速统计分析,看看它是如何分布的。

答案 9 :(得分:0)

对于随机哈希值,一些工程师说黄金比例素数(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. 记录尚未映射的存储桶,即一个空存储桶;
  6. 找出所有铲斗的最大碰撞次数;即最长的链条长度;

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

一些用于整数的哈希函数被认为是好的,但是测试结果表明,当total_data_entry / total_bucket_number = 3时,最长的链长大于10(最大碰撞数> 10),并且许多存储桶未映射(空存储桶),与空载存储桶为零,且黄金比例素数散列产生的最长链长3相比,这是非常糟糕的。

顺便说一句,根据我的测试结果,我发现一个版本的shifting-xor hash函数非常好(由mikera共享)。

unsigned int Hash_UInt_M3(unsigned int key)
{
  key ^= (key << 13);
  key ^= (key >> 17);    
  key ^= (key << 5); 
  return key;
}

答案 10 :(得分:0)

自从找到该线程以来,我一直在使用splitmix64(在Thomas Mueller的answer中指出)。但是,我最近偶然发现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还提供了MurmurHash3最后一步中使用的in-depth analysis和64位混合器。