微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何将 djb2 映射到哈希表?

如何解决如何将 djb2 映射到哈希表?

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

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

这当然是在了解哈希表之所以被称为哈希表之前,因为它们使用哈希。我完全不知道如何进行。

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

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

解决方法

您使用模数。如果您说大小始终是 2 的幂,那么您也可以使用按位与计算相同的值。

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

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

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

每个哈希表都可能有冲突。如果它是一个很好的散列函数,那么这些项目会完全随机地放入桶中——就像任何人都知道的那样接近。如果您有 5 个项目和 10 个桶,那么桶 1 中有一个项目的可能性大约为 50%(实际上是 41%)。它有大约 7% 的几率里面有 2 个项目。大约有 0.8% 的几率里面有 3 个项目。

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

如果您开始将 10 个项目放入 10 个桶中,那么这些概率会迅速上升。为了确保概率保持在较低水平,大多数哈希表会在“满”的 50% 到 75% 左右时扩大它们的大小(当项目数除以桶数时,他们会在 0.5 和0.75)。

例如,如果哈希函数不好,您也可以在一个存储桶中包含大量项目

int hash(const char *s) {return 0;}

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

,

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

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

很多的方法可以做到#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 使用的传统算法。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。