【问题标题】:Hash Array Mapped Trie (HAMT)哈希数组映射树 (HAMT)
【发布时间】:2013-11-11 22:44:22
【问题描述】:

我正在努力了解HAMT 的详细信息。我有implemented one myself in Java 只是为了理解。我对 Tries 很熟悉,我想我掌握了 HAMT 的主要概念。

基本上,

两种类型的节点:

键/值

Key Value Node:
  K key
  V value

索引

Index Node:
  int bitmap (32 bits)
  Node[] table (max length of 32)
  1. 为对象生成 32 位哈希。
  2. 一次遍历 5 位哈希。 (0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-31) 注意:最后一步(第 7 步)只有 2 位。
  3. 在每一步中,找到该 5 位整数在位图中的位置。例如integer==5 bitmap==00001
    1. 如果该位为 1,则该部分哈希存在。
    2. 如果该位为 0,则密钥不存在。
  4. 如果键存在,则通过计算位图中 0 和位置之间的 1 的数量来找到它在表中的索引。例如integer==6 bitmap==0101010101 index==3
    1. 如果表指向键/值节点,则比较键。
    2. 如果表指向一个索引节点,则前进一步进入 2。

我不太了解的部分是碰撞检测和缓解。在链接的论文中,他提到了这一点:

然后将现有密钥插入新的子哈希表中,然后 添加了新密钥。每次多使用 5 位哈希 碰撞概率降低 1/32。偶尔 可能会消耗一个完整的 32 位散列,并且必须计算一个新的散列 区分这两个键。

如果我要计算一个“新”哈希并将对象存储在该新哈希中;你怎么能在结构中查找对象?在进行查找时,它不会生成“初始”哈希而不是“重新计算的哈希”吗?

我一定是错过了什么.....

顺便说一句:HAMT 的性能相当不错,在我的测试中它位于哈希图和树图之间。

Data Structure                    Add time   Remove time     Sorted add time Sorted remove time   Lookup time     Size     
Java's Hash Map                   38.67 ms   18 ms           30 ms           15 ms                16.33 ms        8.19 MB        
HAMT                              68.67 ms   30 ms           57.33 ms        35.33 ms             33 ms           10.18 MB       
Java's Tree Map                   86.33 ms   78 ms           75 ms           39.33 ms             76 ms           8.79 MB        
Java's Skip List Map              111.33 ms  106 ms          72 ms           41 ms                72.33 ms        9.19 MB     

【问题讨论】:

    标签: java algorithm data-structures hash trie


    【解决方案1】:

    我认为你可能会错过这篇论文的两个部分。第一个是您引用的位之前的位:

    否则密钥将与现有密钥发生冲突。在这种情况下,现有密钥 必须替换为子哈希表和现有密钥的下一个 5 位哈希 计算。如果仍然有碰撞,则重复此过程,直到没有碰撞 发生。

    因此,如果您在表中有对象 A,并且您添加了发生冲突的对象 B,则其键发生冲突的单元格将是指向另一个子表的指针(它们不会发生冲突)。

    接下来,您链接的论文的第 3.7 节描述了当您运行完前 32 位末尾时生成新哈希的方法:

    哈希函数被定制为提供 32 位哈希。该算法要求 哈希可以扩展到任意数量的位。这是由 重新散列与表示 trie 级别的整数组合的键,零表示 根。因此,如果两个键确实给出了相同的初始哈希,那么重新哈希有一个 进一步碰撞的概率为 2^32 分之 1。

    如果这似乎无法解释任何事情,请说,我会更详细地扩展此答案。

    【讨论】:

    • 谢谢你;我现在还在努力理解它。我会回复你的。
    • 第 3.7 节假定哈希是多个输入的函数,其中 trie 级别作为某种“种子”,但在 Java 和 C# 等语言中,哈希码通常是对象的原始方法,并且因此只有一个输入(对象)。我看不出有任何方法可以使第 3.7 节适用于此类语言,这意味着您能做的最好的事情就是在叶子中存储一个冲突列表并进行线性搜索。
    • 将要放入数据结构的对象包装在 Node 类中,然后将 Node 而不是包装的对象插入 HAMT。然后,您可以在 Node 类上定义必要的多参数哈希函数。
    • 从底层对象生成的哈希仍然是相同的,这意味着从节点生成的任何哈希也将是相同的,除非包含一些不确定的值,例如节点地址。但是您在搜索密钥时不可能知道节点地址,因此您可以插入到无限深度,但您不能再执行查找。我看不出您的建议如何解决问题。
    • 让我们考虑一个具体的例子:我们有两个键,k1 和 k2,其中 k1!=k2,它们的冲突哈希码是 42。告诉我任何可以采用 42 和 trie 级别和确定性的自定义哈希为 k1 和 k2 产生不同的输出。
    【解决方案2】:

    HAMT 是一种出色且高性能的结构,尤其是在需要不可变对象时,即每次修改后都会创建一个数据结构的新副本!

    至于你关于哈希冲突的问题,我找到了一个 C#implementation (现在是错误的),它显示了它是如何工作的:在每次哈希冲突时,密钥都会重新哈希并重试查找递归直到达到最大迭代限制。

    目前我也在探索函数式编程环境中的 HAMP 并学习现有代码。在Haskell as Data.HshMapClojure as PersistenceHashMap 中有几个HAMT 的参考实现。

    网络上还有一些其他更简单的实现不处理冲突,但它们有助于理解这个概念。他们在HaskellOCaml

    我找到了一个nice summary article article,它用图片和指向原始research papers 的链接来描述HAMT,作者是Phil Bagwell。

    相关点:

    在 F# 中实现 HAMT 时,我注意到描述 here 的 popCount 函数实现确实很重要,与链接中下一个答案中描述的幼稚实现相比,它提供了 10-15%。不是很好,但免费午餐。

    相关的 IntMap 结构(Haskell 及其port to F#)在键可以是整数并且它们实现相关的PATRICIA/Radixtrie 时非常好。

    我相信所有这些实现都非常适合在这些示例中学习高效的不可变数据结构和函数式语言——它们真的很相配!

    【讨论】:

      【解决方案3】:

      如果我要计算一个“新”哈希并将对象存储在那个新的 哈希;你怎么能在 结构体?在进行查找时,它不会生成“初始” 哈希而不是“重新计算的哈希”。

      在进行查找时,会使用初始哈希。当初始位 哈希已用尽,以下任一条件为真:

      1. 我们最终得到一个键/值节点 - 返回它
      2. 我们最终得到一个索引节点 - 这是我们必须去的提示 通过重新计算新的哈希值来加深。

      这里的关键是哈希位用尽

      【讨论】:

        【解决方案4】:

        碰撞的几率可能非常低,通常只对大树有问题。鉴于此,您最好将冲突存储在叶子的数组中并线性搜索 (I do this in my C# HAMT)。

        【讨论】:

        • 虽然这不符合 HAMT 的严格定义,但它可能是最好的方法;我将测量携带另一个数组或链表的额外内存成本。我会告诉你的。
        猜你喜欢
        • 2017-03-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-08-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-11
        相关资源
        最近更新 更多