哈希数组映射树(HAMT)

30

我正试图理解HAMT的细节。为了更好地理解,我已经在Java中自己实现了一个。我对Tries很熟悉,我认为我已经理解了HAMT的主要概念。

基本上,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     
4个回答

8

HAMT是一种非常高效的数据结构,尤其适用于需要不可变对象的场景,即在每次修改数据结构后都会创建一个新副本!

至于你关于哈希冲突的问题,我发现了一个C# 实现(目前有缺陷),展示了它的工作原理:每当出现哈希冲突时,会重新对键进行哈希,并递归地重试查找,直到达到最大迭代次数限制。

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

网上有一些更简单的实现,它们不涉及冲突,但对于理解概念很有用。这里有HaskellOCaml

我找到了一篇很好的综述文章,其中有图片和链接指向Phil Bagwell的原始研究论文

相关点:

在实现F#中的HAMT时,我注意到popCount函数的实现在这里所描述的真的很重要,并且相对于链接中下一个答案中描述的天真实现而言,可以提高10-15%。虽然不是很大,但还是可以免费获得收益。
相关的IntMap结构(Haskell以及其F#端口)非常适用于关键字可能为整数的情况,并且它们实现了相关的PATRICIA / Radix trie
我相信,所有这些实现都非常适合通过这些示例学习高效的不可变数据结构和功能语言 - 它们真的一起闪耀!

谢谢!有点难过这种回答只能得到几个赞,而其他(非常简单的)回答却得到了数百个赞 LOL。你知不知道有没有基于Java的实现? - juanmf
来自Clojure的代码可能可以在Java中运行。但我不经常使用JVM并且不太了解它的生态系统。 - V.B.
阅读有关 HAMT 的文章,我不明白它如何映射子单词。假设两个键都是 (0101; 010101)。第一个键也是第二个键的路径,但没有标记为键。 - juanmf

7
我认为你可能错过了论文中的两个部分。第一部分是你引用的部分之前的部分:
如果关键字与已有关键字冲突,那么现有的关键字必须被替换为子哈希表,并计算现有关键字的下一个5位哈希值。如果仍然存在冲突,则重复此过程,直到没有冲突发生。
因此,如果您在表格中有对象A并添加了冲突的对象B,则它们的键冲突的单元格将指向另一个子表格(在该位置上它们不会冲突)。
接下来,你链接的论文中的第3.7节描述了当你耗尽了第一个32位时生成新哈希的方法:
哈希函数被定制为给出32位哈希值。该算法要求哈希可以扩展到任意数量的位。这是通过将连续的哈希与表示trie级别的整数组合重新哈希来实现的,其中零是根。因此,如果两个关键词确实给出相同的初始哈希值,则重新哈希具有1/2^32的进一步碰撞概率。
如果这似乎没有解释清楚,请说一声,我会提供更多细节。

谢谢你的帮助;我现在还在努力理解它。我会再回复你的。 - Justin
1
第3.7节假设哈希是多个输入的函数,其中trie级别是某种“种子”,但在Java和C#等语言中,哈希码通常是对象上的原始方法,因此只有一个输入(对象)。我看不出如何使第3.7节适用于这些语言,这意味着你所能做的最好的事情就是在叶子节点处线性存储冲突列表并进行搜索。 - naasking
将被放入数据结构的对象封装在一个“Node”类中,然后将“Node”而不是封装的对象插入HAMT。您可以在“Node”类上定义必要的多参数哈希函数。 - Andy Jones
1
让我们考虑一个具体的例子:我们有两个键,k1和k2,其中k1!= k2,并且它们的哈希码冲突是42。请展示给我一个customHash函数,它可以接受42和trie层级,并且能够确定性地为k1和k2产生不同的输出。 - naasking
哪些编程语言具有适合的原始哈希码? - Andy Jones
显示剩余8条评论

1
如果我计算一个“新”的哈希并将对象存储在该新哈希中,您如何能够在结构中查找对象?在查找时,不会生成“重新计算的哈希”,而是生成“初始”哈希。 进行查找时使用初始哈希。当初始哈希中的位耗尽时,以下条件之一成立: 1. 我们最终得到一个键/值节点-返回它 2. 我们最终得到一个索引节点-这是我们必须通过重新计算新哈希来深入的提示。 关键在于哈希位的耗尽。

0

碰撞的概率可能非常低,通常只对巨大的树有问题。鉴于此,最好只是在叶子节点处将碰撞存储在数组中,并进行线性搜索(我在我的C# HAMT中就是这样做的)。


1
虽然这不完全符合 HAMT 的严格定义,但可能是最好的方法;我将测量携带另一个数组或链表的额外内存成本。我会告诉你的。 - Justin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接