字符串的哈希函数


124

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

我尝试的第一个功能是添加ascii代码并使用模(%100),但是在第一次数据测试中我得到的结果很差:130个单词40次冲突。

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

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

提前致谢 !

:-)


11
如果您的哈希表有1万个条目,为什么要使用模100?这么小的模数,从130个单词中找出40个冲突就不足为奇了。
凯里·格雷戈里

13
请参阅burtleburtle.net/bob/hash/evahash.htmlpartow.net/programming/hashfunctions,它们是有关各种哈希(从常规到字符串再到加密)的资源。

3
要澄清@CareyGregory:您确实意识到,作为一个基本的数学真理,在100个存储桶中的130个项目(即mod 100)必须产生30次碰撞(其中将碰撞次数计为第二次,第三次等等)。一个水桶),对吗?所以您只比那高一点。
德罗伯特

4
@lilawood:好的,这就是我的想法,但是要进行更好的测试,应该使用80个单词以及100个条目的哈希表。这将为您提供与实时数据相同的比例,并且不会强制发生冲突。
凯里·格雷戈里

Answers:


185

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

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;
}

37
答案中链接的页面非常有趣。
Adrien Plisson

2
程序如何在while循环之外运行?= S
Daniel N.

1
@ danfly09当c为零时。while(c = * str ++)的等价于(0!=(c = * str ++))
rxantos

5
@Josepas哈希函数在理想情况下应返回a size_t或其他此类无符号值(例如此代码中的unsigned long)。该来电者是负责接收结果的模它适合哈希表。调用者控制要散列到的表插槽;不是功能。它只是返回一些未签名的数字。
WhozCraig

6
惊人。该算法击败了Murmur哈希,FNV变体哈希和许多其他东西!+1
David Haim

24

首先,您通常希望对哈希表使用加密哈希。这是一个算法非常快的加密标准仍是哈希表的标准速度奇慢。

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

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

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

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

编辑:还请注意,对于哈希表大小,10000个插槽很少是一个不错的选择。通常,您需要以下两点之一:您想要一个素数作为大小(需要确保某种类型的哈希解析正确无误),或者想要2的幂(因此可以通过简单的方法将值减小到正确的范围)位掩码)。


这不是c,但我会对您对此相关答案的想法感兴趣:stackoverflow.com/a/31440118/3681880
Suragch 2015年

1
@Suragch:自从我写这篇文章以来,相当多的处理器已经开始包括任一种特殊的硬件来加速SHA计算,这使其更具竞争力。就是说,我怀疑您的代码是否像您想象的那样安全—例如,IEEE浮点数具有两个不同的位模式(0和-0),它们应该产生相同的哈希值(它们将彼此相等) )。
杰里·科芬

@Jerry Coffin我需要哪个库的rol()函数?
thanos.a

@ thanos.a:我不知道它在库中,但是自己滚动只需要一行或两行代码。向左移动一个块,向右移动另一个块,或将它们一起移动。
杰里·科芬

8

Wikipedia显示了一个很好的字符串哈希函数,称为Jenkins One At A 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;
}

8

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

但是,如果数据集是静态的,则最好的解决方案可能是使用完美的hashgperf将为您生成给定数据集的完美哈希。


hsearch通过比较字符串或字符串ptr地址进行搜索?我认为这只是检查ptr地址?我尝试使用不同的指针,但使用相同的字符串。hsearch失败,说明没有元素发现
MK ..

3

对于此466k英语字典,djb2有317个冲突,而MurmurHash对于64位哈希没有任何冲突,而对于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倍。分配更多的值将使您的哈希表更快,但收益迅速减少,使哈希表小于此值将使其指数级地变慢。这是因为哈希表在空间和时间复杂度之间存在非线性权衡,显然最佳负载因子为2-sqrt(2)= 0.58...。


2

首先,将130个单词的40次冲突散列为0..99不好吗?如果您没有采取专门的步骤来实现它,就不能指望完美的哈希。大多数情况下,普通哈希函数的冲突不会比随机生成器少。

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

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


1
哈希冲突的预期数量为n - m * (1 - ((m-1)/m)^n) = 57.075...。40次碰撞好于偶然的预期(46到70次,p值为0.999)。所讨论的哈希函数比它是随机的还是我们目睹了一个非常罕见的事件更为统一。
Wolfgang Brehm

2

虽然djb2,正如cnicutar在stackoverflow上提出的,虽然几乎可以肯定更好,但我认为也值得展示K&R哈希值:

1)显然是一种可怕的哈希算法,如K&R第一版(来源)所述

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

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

    return hash;
}

2)可能是一种相当不错的哈希算法,如K&R第2版中所述(我在本书第144页上进行了验证);注意:% HASHSIZE如果您打算在散列算法之外执行将模数调整为您的数组长度,请确保从return语句中删除。另外,我建议您使用return和“ hashval”类型unsigned long而不是简单的unsigned(int)类型。

unsigned hash(char *s)
{
    unsigned hashval;

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

请注意,从这两种算法中可以明显看出,第1版哈希值如此糟糕的原因之一是因为它没有考虑字符串的字符顺序,因此hash("ab")将返回与相同的值hash("ba")。这是不是使之与第二版散,然而,对于这些字符串这将(好多了!)返回两个不同的值。

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

码:

// 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;
}

2

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

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%。


尽管此链接可以回答问题,但最好在此处包括答案的基本部分并提供链接以供参考。如果链接的页面发生更改,仅链接的答案可能无效。
thewaywere是

为了获得一个好的表格,将每个哈希值的源代码发布到您的答案中也是必不可少的。否则,链接可能会断开,我们很不走运。
加布里埃尔·斯台普斯

如果哈希是真正随机的,则预期的碰撞次数应为9.112499989700318E + 7或0.103 *960³,因此如果它们都在该值附近,我不会感到惊讶,但是0.0616 *960³似乎有点偏离,几乎就像散列比偶然预期的散布更均匀,并且在64字节长的情况下,绝对应该达到此限制。您可以共享散列的一组字符串,以便我尝试重现它吗?
Wolfgang Brehm

0

以下是我使用的效果很好的一件事(我不知道它是否已经提及,因为我不记得它的名字了)。

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


如果我没记错的话,这会遇到与加百列答案中的K&R 1st相同的问题。即“ ab”和“ ba”将散列为相同的值。
约翰·奥斯卡松
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.