【问题标题】:Chained Hash Tables vs. Open-Addressed Hash Tables链式哈希表与开放寻址哈希表
【发布时间】:2011-02-03 02:38:52
【问题描述】:

有人能解释一下这两种实现的主要区别(优点/缺点)吗?

对于一个库,推荐什么实现方式?

【问题讨论】:

    标签: data-structures hashtable


    【解决方案1】:

    Wikipedia's article on hash tables 对人们使用的不同哈希表方案提供了明显更好的解释和概述,这比我想象的要好。实际上,您最好阅读那篇文章而不是在这里提出问题。 :)

    也就是说……

    链式哈希表索引到指向链表头部的指针数组。每个链表单元都有为其分配的键和为该键插入的值。当您想从其键中查找特定元素时,该键的哈希用于计算出要遵循的链表,然后遍历该特定列表以找到您所追求的元素。如果哈希表中的多个键具有相同的哈希,那么您将拥有包含多个元素的链表。

    链式散列的缺点是必须遵循指针才能搜索链表。好处是链式哈希表只会随着负载因子(哈希表中元素与桶数组长度的比率)的增加而线性变慢,即使它超过 1。

    一个开放寻址哈希表索引到一个指向(键,值)对的指针数组。您可以使用键的散列值来确定首先查看数组中的哪个槽。如果散列表中的多个键具有相同的散列,那么您可以使用某种方案来决定要查找的另一个槽。例如,线性探测是您在选择的一个插槽之后查看下一个插槽,然后再查看其后的下一个插槽,依此类推,直到您找到一个与您正在寻找的键匹配的插槽,或者您找到一个空的插槽(在这种情况下,钥匙不能在那里)。

    当负载因子较低时,开放寻址通常比链式散列更快,因为您不必跟踪列表节点之间的指针。如果负载因子接近 1,它会变得非常非常慢,因为您最终通常必须搜索存储桶数组中的许多插槽,然后才能找到您要查找的键或空插槽。此外,哈希表中的元素永远不能多于桶数组中的条目。

    为了处理所有哈希表在负载因子接近 1 时至少会变慢(在某些情况下实际上完全崩溃)这一事实,实际的哈希表实现使存储桶数组更大(通过分配一个新的存储桶数组,并且当负载因子超过某个值(通常约为 0.7)时,将元素从旧元素复制到新元素中,然后释放旧元素)。

    以上所有内容都有很多变化。再一次,请看维基百科的文章,真的很不错。

    对于打算供其他人使用的库,我会强烈建议进行试验。由于它们通常对性能至关重要,因此您通常最好使用已经仔细调整过的其他人的哈希表实现。有很多开源 BSD、LGPL 和 GPL 许可的哈希表实现。

    例如,如果您正在使用 GTK,那么您会发现有一个很好的 hash table in GLib

    【讨论】:

    • 很好的解释。我最近了解到的一件事,大多数摘要都忽略了指出,删除会对开放寻址表的性能产生不利影响。当您删除时,您只需将该条目标记为已删除。插入时,您可以重新使用已删除的条目,但在搜索时,您不能在已删除的条目上停止。如果您进行大量插入和删除,那么随着时间的推移,您会累积已删除的条目,这些条目会影响负载因子。因此,即使实际负载仍然很低,性能也会下降到 O(n)。如果您不删除,那么开放寻址非常好。
    • @Adrian 仅当您使用这种标记删除的方法时才适用。相反,如果您删除要查找的项目,然后在删除的项目之后重新插入探测序列中的所有元素,则删除速度会较慢,但不一定会影响插入。但是,如果您的实现容易出现集群,那么删除可能会很慢。
    • @AdrianMcCarthy - 我没有注意到这一点,因为当时我也没有想到。调整哈希表删除的性能似乎有点微妙,但在你开始仔细考虑这个问题之前并不明显!
    • @AdrianMcCarthy:写得非常好,我正在研究我自己的 OAHT (link to srcforge) 我也在研究谷歌密集型。首先,您不需要无条件标记为已删除。您只需将“如果属于集群的一部分”标记为已删除。其次 - 这是我打算很快调查的一点 - 你可以“虚假地”运行垃圾收集,例如当负载因子变高时重新散列。我认为这是谷歌的方式。
    • 谢谢@AdrianMcCarthy 是的,没错。我希望将光投射到缓存中作为一个潜在的好处(不仅仅是我们避免“必须遵循指针”)。 但是,请注意,可以将链表存储在连续内存数组中,单维或多维,而开放式寻址算法(如双散列或二次探测)也不能保证内存的连续性。换句话说,我不确定有人会认为 OA 会带来更好的缓存(如果您知道我非常感兴趣的任何参考资料)。
    【解决方案2】:

    我的理解(简单来说)是这两种方法都有利有弊,尽管大多数库都使用链式策略。

    链接方式:

    这里的哈希表数组映射到一个项目的链表。如果碰撞的数量相当少,这是有效的。最坏的情况是O(n),其中 n 是表中的元素数。

    使用线性探针打开寻址:

    当碰撞发生时,移动到下一个索引,直到我们找到一个空位。因此,如果碰撞次数较少,这将非常快速且节省空间。这里的限制是表中的条目总数受数组大小的限制。这不是链式的情况。

    还有另一种方法是用二叉搜索树链接。在这种方法中,当碰撞发生时,它们被存储在二叉搜索树而不是链表中。因此,这里最坏的情况是O(log n)。在实践中,这种方法最适合分布极不均匀的情况。

    【讨论】:

      【解决方案3】:

      由于给出了很好的解释,我将添加从 CLRS 获取的可视化以进一步说明:

      打开寻址:

      链接:

      【讨论】:

      • 我相信你放的第一个图是直接寻址而不是开放寻址的图表。
      • 大部分情况下,分离链法中链表中的节点只指向前,不指向后。
      【解决方案4】:

      开放寻址与分离链接

      如果密钥作为条目保存在哈希表本身中,则线性探测、双重和随机哈希是合适的... 这样做被称为“开放寻址” 它也被称为“封闭哈希”

      另一个想法:哈希表中的条目只是指向链表(“链”)头部的指针;链表的元素包含键... 这被称为“分离链接” 它也被称为“开放哈希”

      通过单独的链接,冲突解决变得容易:如果它不存在,只需在其链接列表中插入一个键 (可以使用比链表更高级的数据结构;但链表在一般情况下工作得很好,我们将看到) 让我们来分析一下这些策略的时间成本

      来源:http://cseweb.ucsd.edu/~kube/cls/100/Lectures/lec16/lec16-25.html

      【讨论】:

        【解决方案5】:

        如果在创建哈希表时不知道要插入到哈希表中的项目数,则链式哈希表比开放式寻址更可取。

        增加负载因子(项目数/表大小)会导致开放寻址哈希表的性能大幅下降,但在链式哈希表中性能只会线性下降。

        如果您正在处理低内存并希望减少内存使用量,请选择开放寻址。如果您不担心内存并且想要速度,请使用链式哈希表。

        如有疑问,请使用链式哈希表。添加比预期更多的数据不会导致性能下降。

        【讨论】:

        • 好吧,如果事先不知道项目的数量,标准方法就是将表格扩大两倍。这可以通过开放寻址或链式哈希表来完成。不需要使用链表,也不需要将其驱动到性能变为线性的状态。
        猜你喜欢
        • 1970-01-01
        • 2018-09-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-03
        • 2016-01-29
        • 2017-05-11
        相关资源
        最近更新 更多