为什么用二叉搜索树实现Hashtable?

时间:2014-04-10 18:46:22

标签: data-structures hashtable binary-search-tree

使用数组实现Hashtable时,我们继承了数组的常量时间索引。使用二进制搜索树实现Hashtable的原因是什么,因为它提供了使用O(logn)的搜索?为什么不直接使用二进制搜索树?

4 个答案:

答案 0 :(得分:16)

如果元素没有total order(即没有为所有对定义“大于”和“小于”,或者元素之间不一致),则无法比较所有元素对,因此你不能直接使用BST,但没有什么阻止你通过哈希值索引BST - 因为这是一个整数值,它显然有一个总顺序(尽管你仍然需要{{3} },有办法处理具有相同哈希值的元素。)

然而,BST相对于哈希表的最大优势之一是元素是有序的 - 如果我们按哈希值排序,元素将具有任意顺序而不是,这种优势将不再适用。

至于为什么人们可能会考虑使用BST而不是数组来实现哈希表,它会:

  • 没有需要调整数组大小的缺点 - 使用数组,您通常使用数组大小​​修改哈希值,并在数组填满时调整数组大小,重新插入所有元素,但是使用BST,您可以直接将不变的哈希值插入BST。

    如果我们希望任何单个操作永远不会花费超过一定的时间(如果我们需要调整阵列的大小,这很可能会发生),这可能是相关的,整体性能是次要的,但可能会更好解决这个问题的方法。

  • 降低哈希冲突的风险,因为您没有使用数组大小​​进行修改,因此可能的哈希值可能会大得多。这样可以降低获得哈希表的最坏情况性能的风险(当大部分元素散列到相同的值时)。

    实际的最坏情况表现取决于您如何解决冲突。这通常使用O(n)最坏情况性能的链表进行。但是我们也可以使用BST实现O(log n)性能(如果带有一些哈希的元素数量高于阈值,则在resolve collision中完成) - 也就是说,让每个元素指向的哈希表数组BST,其中所有元素具有相同的哈希值。

  • 可能使用更少的内存 - 使用数组你不可避免地会有一些空索引,但是使用BST,这些根本就不需要存在。虽然这不是一个明确的优势,但如果它是一个优势的话。

    如果我们假设我们使用不太常见的Java's hash table implementation,这个数组也会有一些空索引,这也需要偶尔调整大小,但这是一个简单的内存副本,而不是需要重新插入所有元素更新了哈希。

    如果我们使用典型的基于指针的BST实现,指针的增加成本似乎会超过在数组中有一些空索引的成本(除非数组特别稀疏,这往往是一个不好的标志无论如何都是一个哈希表。)

但是,由于我个人从未听说过这种做法,所以从预期的O(1)到O(log n)的操作成本增加可能是不值得的。

通常情况下,选择是直接使用BST(没有哈希值)和使用哈希表(使用数组)。

答案 1 :(得分:1)

优点:

  1. 潜在地使用较少的空间b / c我们不会分配大数组
  2. 可以按顺序遍历按键,有时很有用
  3. 缺点:

    1. 您的O(log N)查找时间比链式哈希表的保证O​​(1)差。

答案 2 :(得分:0)

由于哈希表的要求是O(1)查找,如果它具有对数查找时间,则它不是哈希表。当然,由于碰撞是阵列实施的一个问题(好吧,不是可能一个问题),使用BST可以提供这方面的好处。但是,一般情况下,它不值得权衡 - 我无法想到在使用哈希表时你不希望保证O(1)查询时间的情况。

或者,有可能通过BST变体保证对数插入和删除的底层结构,其中数组中的每个索引都具有对BST中相应节点的引用。像这样的结构可能会变得复杂,但可以保证O(1)查找和O(logn)插入/删除。

答案 3 :(得分:0)

我发现这看起来是否有人做过。我想也许不是。

今天早上我提出了一个想法,即将二叉树实现为由index存储的行组成的数组。第1行有1,第2行有2,第3行有4(是的,2的幂)。这种结构的优点是位移,加法或减法可用于遍历树而不是使用额外的内存来存储双向或单向引用。

这将允许您根据某种可输入的输入快速搜索哈希值,以发现该值是否存在于某个其他商店中。或者用于哈希冲突(或部分冲突)搜索。我想不出它的许多其他用途,但对于这些它会非常快。非常可能很多旋转操作完全在cpu缓存中发生,并以精美的线性blob写入主内存。

它的主要用途是对随机性质的输入值进行排序。如果数组中的blob是两个部分,如散列和另一个商店的标识符,则可以非常快速地进行比较并快速插入以发现带有散列值的项目保存在另一个位置的位置(如UUID)文件系统节点或甚至文件名或其他可识别短字符串的字符串。)

我会把它留给其他人去梦想其他方法来使用它,但我正在使用它来进行工作搜索表的图论理论证明,以识别杜鹃循环变体的部分碰撞。

我现在正在研究步行公式,现在是:

i =数组元素的索引

走上去(去父母):

i>>1-(i+1)%2

(显然你可能需要测试我是否为零)

向左走(向左和向左):

i<<1+2

(这个和下一个也需要测试2 ^深度的结构,所以它不会走出边缘并回落到根部)

向右走(向右和向右):

i<<1+1

如您所见,每次步行都是基于索引的简短公式。向左和向右移位和添加,以及向上移动的位移,加法和模数。向下移动的两条指令,4向上移动(在汇编程序中,或如上所述在C和其他HLL运算符表示法中)

编辑: 我可以从进一步的评论中看到,削减插入时间的好处肯定会有好处。但我不认为传统的基于矢量的二叉树会提供与密集版本一样多的好处。密集版本,其中所有节点都在一个连续的数组中,当它被搜索时,自然会以线性方式通过内存,帮助减少缓存未命中,从而减少延迟搜索显着,以及随着访问随机访问而导致内存延迟受到影响的事实。

https://github.com/calibrae-project/bast/blob/master/pkg/bast/bast.go

这是我目前使用WiP来实现我称之为分叉阵列搜索树的状态。为了快速插入/删除而不是通过排序的散列集合进行非常慢的搜索,我认为这对于有大量数据进出结构的情况会有很大的好处,或者更多这一点,有利于更实时的应用。