ChatGPT解决这个技术问题 Extra ChatGPT

字符串的哈希函数

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

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

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

散列字符串的最佳算法是什么?

以及如何确定哈希表的大小?

如果您的哈希表有 10K 个条目,为什么要使用模 100?用这么小的模数从 130 个单词中得到 40 次碰撞并不奇怪。
请参阅 burtleburtle.net/bob/hash/evahash.htmlpartow.net/programming/hashfunctions,其中是有关各种散列(从一般到字符串到加密)的资源。
澄清@CareyGregory:您确实意识到,作为一个基本的数学真理,100 个桶中的 130 个项目(即 mod 100)必须产生 30 次碰撞(其中每次放入第二个、第三个等项目时都会计算碰撞一个桶),对吗?所以你只比那个高一点。
@lilawood:好的,这就是我的想法,但为了更好地测试,您应该使用 80 个单词和 100 个条目的哈希表。这将为您提供与实时数据相同的比例,并且不会强制发生冲突。
Good Hash Function for Strings 的可能重复项

c
cnicutar

我使用 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;
}

答案中链接的页面非常有趣。
程序如何运行出while循环? =S
@danfly09 当 c 为零时。 while(c = *str++) 的等价物是 (0 != (c = *str++))
@Josepas 散列函数理想情况下应该返回一个 size_t 或其他此类无符号值(例如此代码中的无符号长整数)。 调用者 负责将结果取模以适合哈希表。调用者控制被散列到的表槽;不是功能。它只是返回一些无符号数。
惊人。该算法击败了 Murmur 散列、FNV 变体散列和许多其他算法! +1
J
Jerry Coffin

首先,您通常不希望对哈希表使用加密哈希。按照加密标准来说非常快的算法,按照哈希表标准来说仍然非常慢。

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

例如,假设 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:自从我写这篇文章以来,很多处理器已经开始包含任何一种特殊硬件来加速 SHA 计算,这使它更具竞争力。也就是说,我怀疑你的代码是否像你想象的那样安全——例如,IEEE 浮点数有两个不同的位模式(0 和 -0),它们应该产生相同的哈希值(它们会相互比较)。
@Jerry Coffin rol() 函数需要哪个库?
@thanos.a:我不知道它在一个库中,但你自己的滚动只需要一两行代码。向左移动一个块,向右移动另一个块,或者将它们一起移动。
@thanos.a,您可以像 static inline unsigned rol(unsigned r, int k) {return (r << k) | (r >> (32 - k));} 一样手动滚动它(假设为 32 位整数)。至少 x86-64 上的 GCC 确实将其编译为一条指令。
R
RushPL

Wikipedia shows 一个很好的字符串散列函数,称为 Jenkins One At 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;
}

N
Nick Johnson

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

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


hsearch 通过比较字符串或字符串 ptr 地址进行搜索?我认为它只是检查ptr地址?我尝试使用不同的指针但相同的字符串计算。 hsearch 失败,说明未找到任何元素
W
Wolfgang Brehm

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

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... 显然。


G
Gabriel Staples

djb2 不错

尽管 djb2presented on stackoverflow by cnicutar 几乎可以肯定更好,但我认为也值得展示 K&R 散列:

其中一个 K&R 散列很糟糕,一个可能非常好:

显然是一个糟糕的哈希算法,如 K&R 第 1 版(来源)unsigned long hash(unsigned char *str) { unsigned int hash = 0;诠释 c;而 (c = *str++) 哈希 += c;返回哈希;可能是一个相当不错的哈希算法,如 K&R 第 2 版中所述(我在本书的第 144 页验证了);注意:如果您计划在哈希算法之外进行模数调整到您的数组长度,请务必从 return 语句中删除 % HASHSIZE。另外,我建议您将 return 和“hashval”类型设为 unsigned long,甚至更好:uint32_t 或 uint64_t,而不是简单的 unsigned (int)。 unsigned hash(char *s) { unsigned hashval; for (hashval = 0; *s != '\0'; s++) hashval = *s + 31*hashval;返回哈希值 % HASHSIZE; }

请注意,从这两种算法中可以清楚地看出,第一版哈希如此糟糕的一个原因是因为它没有考虑字符串字符 order,因此 hash("ab") 将因此返回与 {2 相同的值}。这 不是 所以对于第 2 版散列,但是,它会(好多了!)为这些字符串返回两个不同的值。

std::unordered_map<> 模板容器哈希表使用的 GCC C++11 哈希函数非常出色。

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

这是对使用什么是 GCC C++11 哈希函数的问题的部分回答,说明 GCC 使用 Austin Appleby (http://murmurhash.googlepages.com/) 的“MurmurHashUnaligned2”实现。

在文件“gcc/libstdc++-v3/libsupc++/hash_bytes.cc”中,在这里(https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/libsupc++/hash_bytes.cc),我发现实施。例如,这是“32 位 size_t”返回值的值(2017 年 8 月 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;
}

Austin Appleby 的 MurmerHash3 是最好的!这甚至比他上面使用的 gcc C++11 std::unordered_map<> 哈希也有所改进。

不仅是所有这些中最好的,而且奥斯汀将 MurmerHash3 发布到公共领域。在此处查看我的其他答案:What is the default hash function used in C++ std::unordered_map?

也可以看看

要试用和测试的其他哈希表算法:http://www.cse.yorku.ca/~oz/hash.html。那里提到的哈希算法:djb2 sdbm loss loss (K&R 1st edition)


A
Andriy Makukha

我想验证卞小宁的回答,可惜他没有贴出他的代码。所以我实现了一个小测试套件,并在 466K English words 列表上运行了不同的小散列函数,以查看每个的冲突数量:

Hash function      |     Collisions | Time (words) | Time (file)
=================================================================
CRC32              |    23 (0.005%) |      112 ms  |      38 ms
MurmurOAAT         |    26 (0.006%) |       86 ms  |      10 ms
FNV hash           |    32 (0.007%) |       87 ms  |       7 ms
Jenkins OAAT       |    36 (0.008%) |       90 ms  |       8 ms
DJB2 hash          |   344 (0.074%) |       87 ms  |       5 ms
K&R V2             |   356 (0.076%) |       86 ms  |       5 ms
Coffin             |   763 (0.164%) |       86 ms  |       4 ms
x17 hash           |  2242 (0.481%) |       87 ms  |       7 ms
-----------------------------------------------------------------
MurmurHash3_x86_32 |    19 (0.004%) |       90 ms  |       3 ms

我包括了两者的时间:单独对所有单词进行哈希处理,并对所有英语单词的整个文件进行一次哈希处理。我还在测试中加入了一个更复杂的 MurmurHash3_x86_32 以供参考。

结论:

在 Intel x86-64 架构上使用流行的 DJB2 哈希函数来处理字符串几乎没有意义。因为它比类似的函数(MurmurOAAT、FNV 和 Jenkins OAAT)具有更多的冲突,同时具有非常相似的吞吐量。 Bernstein 的 DJB2 在短弦上表现特别差。示例冲突:Liz/MHz、Bon/COM、Rey/SEX。

测试代码:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define MAXLINE 2048
#define SEED    0x12345678

uint32_t DJB2_hash(const uint8_t *str)
{
    uint32_t hash = 5381;
    uint8_t c;
    while ((c = *str++))
        hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
    return hash;
}

uint32_t FNV(const void* key, int len, uint32_t h)
{
    // Source: https://github.com/aappleby/smhasher/blob/master/src/Hashes.cpp
    h ^= 2166136261UL;
    const uint8_t* data = (const uint8_t*)key;
    for(int i = 0; i < len; i++)
    {
        h ^= data[i];
        h *= 16777619;
    }
    return h;
}

uint32_t MurmurOAAT_32(const char* str, uint32_t h)
{
    // One-byte-at-a-time hash based on Murmur's mix
    // Source: https://github.com/aappleby/smhasher/blob/master/src/Hashes.cpp
    for (; *str; ++str) {
        h ^= *str;
        h *= 0x5bd1e995;
        h ^= h >> 15;
    }
    return h;
}

uint32_t KR_v2_hash(const char *s)
{
    // Source: https://stackoverflow.com/a/45641002/5407270
    uint32_t hashval = 0;
    for (hashval = 0; *s != '\0'; s++)
        hashval = *s + 31*hashval;
    return hashval;
}

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

uint32_t crc32b(const uint8_t *str) {
    // Source: https://stackoverflow.com/a/21001712
    unsigned int byte, crc, mask;
    int i = 0, j;
    crc = 0xFFFFFFFF;
    while (str[i] != 0) {
        byte = str[i];
        crc = crc ^ byte;
        for (j = 7; j >= 0; j--) {
            mask = -(crc & 1);
            crc = (crc >> 1) ^ (0xEDB88320 & mask);
        }
        i = i + 1;
    }
    return ~crc;
}

inline uint32_t _rotl32(uint32_t x, int32_t bits)
{
    return x<<bits | x>>(32-bits);      // C idiom: will be optimized to a single operation
}

uint32_t Coffin_hash(char const *input) { 
    // Source: https://stackoverflow.com/a/7666668/5407270
    uint32_t result = 0x55555555;
    while (*input) { 
        result ^= *input++;
        result = _rotl32(result, 5);
    }
    return result;
}

uint32_t x17(const void * key, int len, uint32_t h)
{
    // Source: https://github.com/aappleby/smhasher/blob/master/src/Hashes.cpp
    const uint8_t * data = (const uint8_t*)key;
    for (int i = 0; i < len; ++i)
    {
        h = 17 * h + (data[i] - ' ');
    }
    return h ^ (h >> 16);
}

uint32_t apply_hash(int hash, const char* line)
{
    switch (hash) {
    case 1: return crc32b((const uint8_t*)line);
    case 2: return MurmurOAAT_32(line, SEED);
    case 3: return FNV(line, strlen(line), SEED);
    case 4: return Jenkins_one_at_a_time_hash(line, strlen(line));
    case 5: return DJB2_hash((const uint8_t*)line);
    case 6: return KR_v2_hash(line);
    case 7: return Coffin_hash(line);
    case 8: return x17(line, strlen(line), SEED);
    default: break;
    }
    return 0;
}

int main(int argc, char* argv[])
{
    // Read arguments
    const int hash_choice = atoi(argv[1]);
    char const* const fn = argv[2];

    // Read file
    FILE* f = fopen(fn, "r");

    // Read file line by line, calculate hash
    char line[MAXLINE];
    while (fgets(line, sizeof(line), f)) {
        line[strcspn(line, "\n")] = '\0';   // strip newline
        uint32_t hash = apply_hash(hash_choice, line);
        printf("%08x\n", hash);
    }
    fclose(f);

    return 0;
}

PS 可以在 Reini Urban (rurban) 的 SMHasher repository 中找到对现代散列函数的速度和质量的更全面的回顾。请注意表中的“质量问题”列。


P
Pascal Cuoq

首先,130 个单词的 40 次冲突是否散列到 0..99 不好?如果您不采取专门的措施来实现完美的散列,就不能指望完美的散列。大多数时候,普通的散列函数不会比随机生成器发生更少的冲突。

具有良好声誉的散列函数是 MurmurHash3

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


预期的哈希冲突数是 n - m * (1 - ((m-1)/m)^n) = 57.075...。 40 次碰撞比偶然预期的要好(46 到 70,p 分数为 0.999)。所讨论的散列函数比随机或我们正在目睹非常罕见的事件更统一。
S
Sean Bright

我已经尝试了这些哈希函数并得到了以下结果。我有大约 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% 的冲突率。


虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。
赞成一个好的表格,在你的答案中发布每个哈希的源代码也是必不可少的。否则,链接可能会断开,我们就不走运了。
如果哈希是真正随机的,则预期的冲突次数应该是 9.112499989700318E+7 或 0.103 * 960³,所以如果它们都在该值附近,我不会感到惊讶,但 0.0616 * 960³ 似乎有点偏离,几乎就像散列的分布比偶然预期的更均匀,并且在 64 字节长度时绝对应该接近这个限制。你能分享你散列的字符串集,以便我可以尝试重现它吗?
谁知道14s only finish 1/20是什么意思?我不明白那条线。
M
Michael Nett

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

您为密钥字母表中的每个字符 [0,255] 预先计算一个带有随机数的表 T。您通过 T[k0] xor T[k1] xor ... xor T[kN] 对密钥“k0 k1 k2 ... kN”进行哈希处理。您可以轻松地证明这与您的随机数生成器一样随机,并且在计算上非常可行,如果您真的遇到了一个非常糟糕的实例,并且有很多冲突,您可以使用一批新的随机数重复整个过程。


如果我没记错的话,这与加布里埃尔的回答中的 K&R 1st 存在相同的问题;即“ab”和“ba”将散列到相同的值。