为啥用二叉搜索树实现哈希表?

Posted

技术标签:

【中文标题】为啥用二叉搜索树实现哈希表?【英文标题】:Why implement a Hashtable with a Binary Search Tree?为什么用二叉搜索树实现哈希表? 【发布时间】:2014-05-24 15:47:05 【问题描述】:

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

【问题讨论】:

Advantages of Binary Search Trees over Hash Tables 的可能重复项 Abdullah,我特别询问了有关使用二叉搜索树实现 hastable 的问题。不是哈希表与 BST。 什么是带有二叉搜索树的哈希表?键是否散列并存储在数组中以及排列在树中?还是每个桶中的元素存储为树而不是列表? 提问的动机是什么?您是否遇到过使用 BST 实现的假定哈希表?或者可能是在每个存储桶上使用 BST 以更快地搜索冲突? @MiserableVariable 出于所有意图和目的,它使用 BST 实现哈希表。对于最终用户来说,它看起来/工作起来就像一个“真正的”哈希表,尽管性能考虑略有不同。 【参考方案1】:

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

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

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

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

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

降低哈希冲突的风险,因为您不修改数组大小,因此可能的哈希数量可能会大得多。这将降低获得哈希表最坏情况性能的风险(即大部分元素哈希到相同值时)。

实际最差情况的性能取决于您解决冲突的方式。这通常通过 O(n) 最坏情况性能的链表来完成。但是我们也可以使用 BST 实现 O(log n) 性能(如果具有某些哈希的元素数量高于阈值,则在 Java's hash table implementation 中完成)-也就是说,让您的哈希表数组中的每个元素都指向一个 BST其中所有元素都具有相同的哈希值。

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

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

如果我们使用典型的基于指针的 BST 实现,则指针的附加成本似乎超过了在数组中包含几个空索引的成本(除非数组特别稀疏,这往往是一个不好的迹象无论如何都是一个哈希表)。

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

通常情况下,确实可以在直接使用 BST(不带哈希值)和使用哈希表(带数组)之间进行选择。

【讨论】:

虽然我知道你来自哪里,但我认为使用普通哈希表可以更有效地实现任何一个好处(不调整大小插入或更少内存),尽管可能不是两者兼而有之:如果 O( n) 调整大小是令人望而却步的,可以通过拥有两个表并在每次修改上移动少量恒定数量的条目来避免它。同样,如果内存非常宝贵,则开放寻址哈希表可以非常紧密地打包并获得不错(但不是最佳)的性能,这样每个 BST 节点都比数组槽大几倍,并且负载率超过 90% 什么是总订单?还是没看懂,可以举个例子更新一下吗? @AbhimanyuAryan 请参阅:What is total order - explanation please 和 How can you explain partial order and total order in simple terms? 但是,我不确定是否存在没有总订单的集合(以上主要与是否一个特定的订单是一个总订单),并不是说这特别相关,因为当您定义一个订单时,您倾向于这样做是有原因的。 是否有哈希表的 BST 实现而不是链表实现。我很困惑在 BST 上表示键值对。你有什么资源可以让我进行算法或编码吗? @AbhimanyuAryan 正如我在回答中提到的,Java 的 HashMap (code here) 在某些情况下使用 BST 而不是链表。虽然代码很多 - 我可能会建议尝试主要通过 cmets 来理解它(也许this post I linked to 可以提供帮助)。【参考方案2】:

优点:

    可能使用更少的空间 b/c 我们不分配大数组 可以按顺序遍历键,有时很有用

缺点:

    查找时间为 O(log N),比链式哈希表保证的 O(1) 还要糟糕。

【讨论】:

链式哈希表的最坏情况是 O(N)。当每个值都导致冲突时,就会发生这种情况。假设您将数组替换为平衡二叉树或 trie,这可能会导致恒定时间查找,因为键中的位数是固定的,并且不直接取决于散列中的项目数。【参考方案3】:

由于哈希表的要求是 O(1) 查找,如果它具有对数查找时间,它就不是哈希表。当然,由于冲突是数组实现的一个问题(嗯,可能不是问题),使用 BST 可以在这方面提供好处。不过,一般来说,这不值得权衡 - 我想不出在使用哈希表时您不希望保证 O(1) 查找时间的情况。

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

【讨论】:

你只是在争论它叫什么以及它是否存在的语义,而不是真正回答你想要使用它的为什么 时间是散列键长度的对数(它是常数),而不是散列中元素数量的对数。所以它仍然是一个恒定时间算法。 @dgatwood,我认为你错了。哈希键的长度如何对数?如何仅通过查看键的哈希值来找到应该返回树中的哪个节点?那我们为什么要使用二叉搜索树呢?如果不需要搜索,让我们使用二叉树。我认为它是 O(logn),其中 n 是树中元素的数量而不是哈希键的长度。 如果我正确理解了原始问题,它是关于根据哈希结果中的位将存储桶本身存储在二叉树中。当然,我会考虑这样做的原因是通过不存储未使用的存储桶来节省内存,而我相信所描述的方法使用相同数量的内存或更多内存,具体取决于你如何解释它,所以我看不出问题中描述的方法与简单的索引查找相比有什么好处,但这是一个单独的讨论。【参考方案4】:

我发现这个是想看看是否有人做过。我猜可能不会。

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

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

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

我会把它留给其他人去想象其他使用它的方法,但我将它用于图论工作证明搜索表,用于识别 Cuckoo Cycle 变体的部分碰撞。

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

i = 数组元素的索引

走上去(去父母那里):

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

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

向左走(向下和向左):

i<<1+2

(这个和下一个也需要针对结构的 2^depth 进行测试,因此它不会离开边缘并回落到根部)

向右走(向下和向右):

i<<1+1

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

编辑: 我可以从进一步的评论中看到,削减插入时间的好处肯定是有益的。但我不认为传统的基于向量的二叉树会提供与密集版本几乎一样多的好处。一个密集版本,其中所有节点都在一个连续的数组中,当它被搜索时,自然会以线性方式穿过内存,这应该有助于减少缓存未命中,从而减少延迟搜索显着,以及与按顺序通过块进行流式传输相比,随机访问内存存在延迟的事实。

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

这是我目前的在制品状态,用于实现我所说的分叉数组搜索树。为了快速插入/删除而不是通过排序的哈希集合进行非常慢的搜索,我认为这对于有大量数据传入和通过结构的情况,或者更多重点,有利于更多实时应用。

【讨论】:

以上是关于为啥用二叉搜索树实现哈希表?的主要内容,如果未能解决你的问题,请参考以下文章

二叉搜索树

二叉树与链表

二叉查找树BST

第七节1:Java集合框架之二叉排序树和哈希表

二叉搜索树的基本操作

c_cpp 二分搜索是所有以比较为基础的搜索算法时间复杂度最低的算法。用二叉树描速二分查找算法,最坏情况下与二叉树的最高阶相同。比较二叉树线性查找也可用二叉树表示,最坏情况下比较次数为数组元素数量。任