字符串的哈希函数

时间:2011-10-05 19:21:17

标签: c algorithm hash dictionary hashtable

我正在用C语言编写哈希表,我正在测试字符串的哈希函数。

我尝试过的第一个函数是添加ascii代码并使用modulo(%100)但是我在第一次测试数据时得到的结果很差:140个单词的40次碰撞。

最终输入数据将包含8 000个单词(它是一个文件中的dictionnary存储)。哈希表声明为int table [10000],并包含txt文件中单词的位置。

第一个问题是散列字符串的最佳算法是哪一个?以及如何确定哈希表的大小?

提前感谢!

: - )

9 个答案:

答案 0 :(得分:155)

我与Dan Bernstein的djb2取得了不错的成绩。

unsigned long
hash(unsigned char *str)
{
    unsigned long hash = 5381;
    int c;

    while (c = *str++)
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return hash;
}

答案 1 :(得分:19)

首先,您通常想要为哈希表使用加密哈希。根据哈希表标准,加密标准非常的算法仍然非常慢。

其次,您希望确保输入的每一位都能/将影响结果。一种简单的方法是将当前结果旋转一些位,然后用当前字节对当前哈希码进行异或。重复,直到到达字符串的末尾。请注意,您通常希望旋转成为字节大小的偶数倍。

例如,假设8位字节的常见情况,您可以旋转5位:

int hash(char const *input) { 
    int result = 0x55555555;

    while (*input) { 
        result ^= *input++;
        result = rol(result, 5);
    }
}

编辑:另请注意,对于哈希表大小,10000个插槽很少是一个不错的选择。您通常需要以下两种方法之一:您要么使用素数作为大小(需要确保某些类型的散列分辨率的正确性),要么需要2的幂(因此可以通过简单的方法将值减小到正确的范围位掩码)。

答案 2 :(得分:8)

C有许多现有的哈希表实现,从C标准库hcreate / hdestroy / hsearch到APRglib,它们还提供预构建的哈希函数。我强烈建议使用它们,而不是发明自己的哈希表或哈希函数;它们已针对常见用例进行了大量优化。

但是,如果您的数据集是静态的,那么您的最佳解决方案可能是使用perfect hash。对于给定的数据集,gperf将为您生成完美的哈希值。

答案 3 :(得分:7)

Wikipedia shows一个很棒的字符串哈希函数,叫做Jenkins One A Time Hash。它还引用了此哈希的改进版本。

uint32_t jenkins_one_at_a_time_hash(char *key, size_t len)
{
    uint32_t hash, i;
    for(hash = i = 0; i < len; ++i)
    {
        hash += key[i];
        hash += (hash << 10);
        hash ^= (hash >> 6);
    }
    hash += (hash << 3);
    hash ^= (hash >> 11);
    hash += (hash << 15);
    return hash;
}

答案 4 :(得分:2)

首先,是40个冲突,130个单词哈希到0..99不好?如果你没有特别采取措施来实现它,你就不能指望完美的哈希。普通哈希函数在大多数情况下不会比随机生成器具有更少的冲突。

具有良好声誉的哈希函数是MurmurHash3

最后,关于哈希表的大小,它实际上取决于您考虑的哈希表类型,尤其是桶是可扩展还是单槽。如果存储桶是可扩展的,那么还有一个选择:您可以选择存储器/速度限制的平均存储桶长度。

答案 5 :(得分:2)

我尝试过这些哈希函数并得到以下结果。我有大约960 ^ 3个条目,每个64字节长,64个字符以不同的顺序,哈希值32位。来自here的代码。

Hash function  |  collision rate | how many minutes to finish
MurmurHash3    |    6.?%         |       4m15s
Jenkins One..  |    6.1%         |       6m54s   
Bob, 1st in link|   6.16%        |       5m34s
SuperFastHash  |    10%          |       4m58s
bernstein      |    20%          | 14s only finish 1/20
one_at_a_time  |    6.16%        |       7m5s
crc            |    6.16%        |       7m56s

一个奇怪的事情是,几乎所有哈希函数对我的数据都有6%的冲突率。

答案 6 :(得分:2)

虽然djb2presented on stackoverflow by cnicutar几乎肯定更好,但我认为值得展示K&R哈希值:

1)显然是一个糟糕的哈希算法,如K&amp; R第1版(source)中所示

unsigned long hash(unsigned char *str)
{
    unsigned int hash = 0;
    int c;

    while (c = *str++)
        hash += c;

    return hash;
}

2)可能是一个相当不错的哈希算法,如K&amp; R版本2中所示(由我在本书第144页验证);注意:如果您计划在哈希算法之外进行模数调整到数组长度,请务必从return语句中删除% HASHSIZE。另外,我建议你做回报和&#34; hashval&#34;键入unsigned long而不是简单unsigned(int)。

unsigned hash(char *s)
{
    unsigned hashval;

    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31*hashval;
    return hashval % HASHSIZE;
}

请注意,从两种算法中可以清楚地看出,第一版哈希非常糟糕的一个原因是因为它没有考虑字符串字符 order ,所以hash("ab")因此将返回与hash("ba")相同的值。这是所以使用第二版哈希,这会(更好!)为这些字符串返回两个不同的值。

用于unordered_map(哈希表模板)和unordered_set(哈希集模板)的GCC C ++ 11哈希函数似乎如下所示。 < / p>

代码:

// Implementation of Murmur hash for 32-bit size_t.
size_t _Hash_bytes(const void* ptr, size_t len, size_t seed)
{
  const size_t m = 0x5bd1e995;
  size_t hash = seed ^ len;
  const char* buf = static_cast<const char*>(ptr);

  // Mix 4 bytes at a time into the hash.
  while (len >= 4)
  {
    size_t k = unaligned_load(buf);
    k *= m;
    k ^= k >> 24;
    k *= m;
    hash *= m;
    hash ^= k;
    buf += 4;
    len -= 4;
  }

  // Handle the last few bytes of the input array.
  switch (len)
  {
    case 3:
      hash ^= static_cast<unsigned char>(buf[2]) << 16;
      [[gnu::fallthrough]];
    case 2:
      hash ^= static_cast<unsigned char>(buf[1]) << 8;
      [[gnu::fallthrough]];
    case 1:
      hash ^= static_cast<unsigned char>(buf[0]);
      hash *= m;
  };

  // Do a few final mixes of the hash.
  hash ^= hash >> 13;
  hash *= m;
  hash ^= hash >> 15;
  return hash;
}

答案 7 :(得分:1)

对于this 466k english dictionary

djb2有317个冲突,而对于64位哈希,MurmurHash没有,而对于32位哈希,则21个(对于466k随机32位哈希,大约25个)。 我的建议是使用MurmurHash(如果可用),它非常快,因为它一次占用几个字节。但是,如果您需要一个简单且简短的哈希函数来复制并粘贴到您的项目中,我建议使用杂音(一次一字节):

uint32_t inline MurmurOAAT32 ( const char * key)
{
  uint32_t h(3323198485ul);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e995;
    h ^= h >> 15;
  }
  return h;
}

uint64_t inline MurmurOAAT64 ( const char * key)
{
  uint64_t h(525201411107845655ull);
  for (;*key;++key) {
    h ^= *key;
    h *= 0x5bd1e9955bd1e995;
    h ^= h >> 47;
  }
  return h;
}

哈希表的最佳大小是-简而言之-尽可能大,同时仍适合内存。因为我们通常不知道或不想查询可用的内存量,甚至可能会更改,所以最佳哈希表大小大约是表中要存储的元素数量的2倍。分配更多的值将使您的哈希表更快,但收益迅速减少,使哈希表小于此值将使其指数级地变慢。这是因为哈希表存在一个非线性trade-off between space and time complexity,最佳负载因子显然是2-sqrt(2)= 0.58...。

答案 8 :(得分:0)

我使用的一件好事是以下(我不知道是否已经提到它因为我记不起它的名字了。)

您为密钥的字母[0,255]中的每个字符预先计算了一个带有随机数的表T.通过取T [k0] xor T [k1] xor ... xor T [kN]来哈希你的密钥'k0 k1 k2 ... kN'。你可以很容易地证明这与你的随机数生成器一样随机,并且计算上非常可行。如果你真的遇到了很多碰撞的非常糟糕的实例,你可以用一批新的随机数重复整个事情。