哈希数组映射三重奏 (HAMT)

2022-09-01 12:15:08

我正试图了解HAMT的细节。我自己在Java中实现了一个,只是为了理解。我熟悉Trys,我想我了解HAMT的主要概念。

基本上

两种类型的节点:

键/值

Key Value Node:
  K key
  V value

指数

Index Node:
  int bitmap (32 bits)
  Node[] table (max length of 32)
  1. 为对象生成 32 位哈希。
  2. 一次单步执行 5 位哈希。 注意:最后一步(第7步)只有2位。(0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-31)
  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     

答案 1

HAMT是一个伟大而高性能的结构,特别是当需要不可变对象时,即每次修改后都会创建数据结构的新副本!

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

目前,我还在函数式编程环境中探索HAMP并学习现有代码。HAMT在Haskell中有几个参考实现,作为Data.HshMap,在Clojure中作为PersistenceHashMap

Web上还有其他一些更简单的实现不处理冲突,但它们对于理解概念很有用。在这里,他们在HaskellOCaml

我找到了一篇很好的摘要文章,用图片和Phil Bagwell的原始研究论文链接来描述HAMT。

相关要点:

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

相关的IntMap结构(Haskell及其到F#的端口)非常好,当键可以是整数并且它们实现相关的PATRICIA / Radix trie时。

我相信所有这些实现都非常好,可以在这些例子中学习有效的不可变数据结构和函数式语言 - 它们真的一起闪耀!


答案 2

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

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

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

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

哈希函数经过定制,可提供 32 位哈希。该算法要求哈希可以扩展到任意位数。这是通过将键与表示 trie 级别的整数(零是根)重新哈希处理来实现的。因此,如果两个键确实给出了相同的初始哈希值,则重述的概率为 1/2^32 的进一步冲突。

如果这似乎没有解释任何事情,说出来,我将更详细地扩展这个答案。