我如何将 djb2 映射到哈希表?

Posted

技术标签:

【中文标题】我如何将 djb2 映射到哈希表?【英文标题】:How would I map djb2 to a hash table? 【发布时间】:2021-10-05 03:15:47 【问题描述】:

我有一个作业(CS50 - 拼写器),我必须在其中实现一个带有链表的哈希表。 然而,对于额外的挑战,我们也被要求实现散列算法,我对散列表和散列完全陌生,并且对密码学了解 0;在阅读了一段时间后,我找到了djb2 hash,我认为它可以很好地与我的数据集(一个包含 143k 小写单词的字典(有些带有'))配合使用,我将不得不使用它来检查其他数据集.

分析数据集后,我最初的想法是将其拆分为前三个字母,然后有一个(我的数据集元素上共有 3 个字母变体)包含 3 个字符的数组,其中包含每个单词的二进制三个链表的头部. (我不能这样做,因为练习已经包含了一个 sllist 的结构和一个散列函数的原型)

这当然是在学习哈希表被称为因为它们使用哈希之前。我完全被蒙住了眼睛。

我看到人们经常使用 mod % 将其映射到他们的列表,但这让我感到困惑,因为您如何保证不会有更多的冲突,以及将它们最小化的最佳数组大小是多少?

如何将 djb2 函数的结果映射到哈希表?我的情况有更好的方法吗?

【问题讨论】:

有效散列函数唯一需要的是它为所有输入和X==Y => h(X)==h(Y) 生成一个数字。即使是所有冲突(例如h(X)=0),您的代码也应该正确运行(尽管速度很慢)。作为第二步,一个好的散列函数应该尽量减少冲突。如果您有一个特定的数据集,您可以测量不同哈希函数和存储桶数量发生了多少次冲突。 但是是的,如果你有 N 个桶和一个返回任意整数的哈希函数,%N 是常用的方法。 “你怎么能保证……?”我们不可以。没有哈希函数可以完美地处理所有可能的数据。测量。 【参考方案1】:

你使用模数。如果你说大小总是 2 的幂,那么你也可以使用按位与来计算相同的东西。

你怎么能保证不会有更多的冲突,以及将它们最小化的最佳数组大小是多少?

你不能。没有人知道。除非它应该尽可能大,但不要大到占用你所有的内存。

哈希表基本上是概率数据结构。无法确保它们在各个方面都完全、100% 完美。你只能得到“足够完美”,这通常是 95% 的完美。如果你 5% 的桶里有两件东西……大不了,谁在乎呢。 95% 的情况下您只需检查一项,而在 5% 的情况下您仍然只需检查两项。

每个哈希表都可能有冲突。如果它是一个好的散列函数,那么这些项目会完全随机地放入桶中 - 任何人都知道的尽可能接近。如果您有 5 个项目和 10 个存储桶,那么存储桶 1 中有大约 50% 的机会包含一个项目(实际上是 41%)。有大约 7% 的机会它有 2 个项目。大约有 0.8% 的机会它有 3 个项目。

处理这个问题的方法是确保你的哈希表可以在同一个存储桶中有多个项目,但它不必很快,因为它不会发生常常。链表是一种方式。一个更好的方法(因为 CPU 缓存)是使用下一个桶来代替,这称为 开放寻址,但它很复杂。

如果您开始将 10 件物品放入 10 个桶中,那么这些概率会迅速上升。为了确保概率保持在较低水平,大多数哈希表会在它们“满”的大约 50% 到 75% 时扩展它们的大小(当项目数除以存储桶数,超过他们在 0.5 到 0.5 之间选择的某个数字时) 0.75)。

如果你的哈希函数不好,你也可以在一个桶里有很多项目,例如

int hash(const char *s) return 0;

无论您的哈希表如何尝试分配它们 - 无论它使用模数还是其他方式,都会将每个项目放入同一个存储桶中。这就是为什么一个好的哈希函数是必不可少的。

【讨论】:

【参考方案2】:

我相信关于哈希函数你需要了解三件事:

    您想将一个 N 字节字符串归结为一个 int 数字。 您想进一步将一个-int 数字归结为哈希表中“桶”的数量。为此选择的工具当然是模运算符%。 要做到这一点非常困难,但如果您刚刚开始,即使是糟糕的哈希函数也可以。

很多种方法来做#1。您可以将字符串中字符的字节值相加:

unsigned int hash1(const char *str)

    unsigned int hash = 0;
    unsigned char *p;
    for(p = str; *p != '\0'; p++)
        hash += *p;
    return hash;

或者您可以将字符串中字符的字节值进行异或:

unsigned int hash2(const char *str)

    unsigned int hash = 0;
    unsigned char *p;
    for(p = str; *p != '\0'; p++)
        hash ^= *p;
    return hash;

(跳到第 3 点,这两个结果都非常糟糕,但暂时可以。)

在调用者中,您通常获取其中一个人的返回值,并使用% 将其转换为哈希表的索引:

#define HASHSIZE 37
HashtabEnt hashtab[HASHSIZE];

// ...

unsigned ix = hash(string) % HASHSIZE;
x = hashtab[ix];

// ...

然后最大的问题是,如何编写一个好的散列函数?这实际上是一个具有相当大和持续的理论兴趣的领域,我不是专家,所以我不会试图给你一个完整的治疗。至少你需要确保输入的每个字节对输出都有一些影响。理想情况下,您希望能够生成完全覆盖输出范围的值。优选地,它将生成覆盖具有合理均匀分布的输出范围的输出值。如果您需要加密安全的散列,您将有额外的要求,但对于简单的字典式散列,您不必担心这些。

我上面的函数hash2 很糟糕,因为它永远不会生成大于 255 的哈希值(即超过 8 位,因此它可能在“完全覆盖输出范围”时失败)。 hash1 也好不了多少,因为除非输入字符串很大,否则它不会超过 8 位。一个简单的改进是结合移位和异或:

unsigned int hash3(const char *str)

    unsigned int hash = 0;
    unsigned char *p;
    for(p = str; *p != '\0'; p++)
        hash = (hash << 1) ^ *p;
    return hash;

但这也不好,因为它总是将位移到左边,这意味着最终的哈希值最终只是最后几个输入字节的函数,而不是全部——也就是说,它“输入的每个字节对输出都有一定的影响”失败。

所以另一种方法是进行循环移位,然后对下一个字节进行异或:

unsigned int hash4(const char *str)

    unsigned int hash = 0;
    unsigned char *p;
    for(p = str; *p != '\0'; p++)
        hash = ((hash << 1) & 0xffff | (hash >> 15) & 1) ^ *p;
    return hash;

这是Unix "sum" command使用的传统算法。

【讨论】:

以上是关于我如何将 djb2 映射到哈希表?的主要内容,如果未能解决你的问题,请参考以下文章

以整数为键并映射到 void 指针的 C 映射/哈希表

如何知道映射表中是不是存在特定值?

超高性能 C/C++ 哈希映射(表、字典)[关闭]

哈希算法从原理到实战

哈希表及其应用分析

哈希表及其应用分析