二进制搜索树比哈希表的优势


101

与哈希表相比,二进制搜索树有什么优势?

哈希表可以在Theta(1)时间内查找任何元素,添加元素也很容易.....但是我不确定反过来的好处。


对于哈希表,find()insert()和remove()的运行时间是多少?theta(1)theta(1)和theta(1)对吗?
致力于2010年

8
几乎总是这样。如果您遇到很多冲突,那么这些时间可能会增加到O(n)。
克里斯汀·曼

1
这些时间还取决于您的哈希函数。如果出于某种奇怪的原因而不是O(1),则显然您的操作将具有最低的界限,无论您的哈希函数运行的效率如何。
克里斯蒂安·曼

我想说BST的最大优点是它在排序的数据结构中。详细用例已在此处列出。
元涛

Answers:


93

请记住,二叉搜索树(基于引用)是内存有效的。他们不会保留比他们需要更多的内存。

例如,如果哈希函数具有range R(h) = 0...100,那么即使您仅哈希20个元素,也需要分配100个(指向)元素的数组。如果要使用二进制搜索树存储相同的信息,则只会分配所需的空间以及一些有关链接的元数据。


33
哈希函数输出的整个范围必须存在于数组中是不正确的。可以通过数组的长度简单地修改哈希值,以允许使用更小的数组。当然,可能不知道要添加的元素的最终数量,因此哈希表可能仍会分配比所需更多的空间。但是,二叉搜索树可能浪费更多或更多的内存。链接的实现每个元素至少需要两个额外的指针(如果使用父指针,则为三个),而基于数组的BST可能会浪费大量内存来填充树的未填充部分。
Solaraeus

4
@Solaraeus:基于数组的BST最好与哈希表进行比较,它们不比哈希表浪费更多。与重新计算整个表相比,您还可以扩展BST而不只是存储副本。
Guvante 2012年

125

其他人没有指出的一个优点是,二进制搜索树使您可以有效地进行范围搜索。

为了说明我的想法,我想举一个极端的例子。假设您要获取所有键在0到5000之间的元素。实际上,只有一个这样的元素,还有10000个其他键不在此范围内的元素。BST可以非常有效地进行范围搜索,因为它不搜索不可能获得答案的子树。

同时,如何在哈希表中进行范围搜索?您要么需要遍历每个存储桶空间(即O(n)),要么必须查找1,2,3,4 ...是否多达5000个。(0和5000之间的键是一个无限集吗?例如,键可以是小数)


11
BST可以有效地进行范围搜索!对我来说,这是实用和算法方法的最佳答案。
2013年

4
哇,这真的解释了为什么树与数据库如此相关;当您需要执行基于键的过滤时,它们的优势最为明显。使用哈希映射,您需要遍历所有键来解决“找到键在1000到3290之间的所有项目”
德米特里(Dmitry),

77

二叉树的一个“优点”是可以遍历它以按顺序列出所有元素。对于哈希表,这不是不可能的,但不是设计成哈希结构的正常操作。


3
在哈希表上以任何顺序遍历可能毫无意义。
FrustratedWithFormsDesigner 2010年

2
@FrustratedWithFormsDesigner。请参阅排序的线性哈希表
NealB 2010年

感谢您的链接,这是一个有趣的想法!我认为我从未见过或使用过这种实现(不为人所知)。
FrustratedWithFormsDesigner 2010年


51

除了所有其他好的评论:

与二叉树相比,哈希表通常具有更好的缓存行为,需要较少的内存读取。对于哈希表,通常只有在访问拥有数据的引用之前,才需要进行一次读取。如果二进制树是平衡变体,则它需要k * lg(n)数量级的内存来读取某个常数k。

另一方面,如果敌人知道您的哈希函数,则该敌人可以强制您的哈希表进行碰撞,从而大大降低其性能。解决方法是从一个族中随机选择哈希函数,但是BST没有此缺点。另外,当哈希表压力过大时,您通常倾向于扩大并重新分配哈希表,这可能是一项昂贵的操作。BST的行为在这里更简单,并且不会突然分配大量数据并进行重新哈希操作。

树往往是最终的平均数据结构。它们可以充当列表,可以轻松拆分以进行并行操作,可以按照O(lg n)的顺序快速移除,插入和查找。他们没有做得特别好,但是也没有任何过分的不良行为。

最后,与散列表相比,BST在(纯)功能语言中更容易实现,并且它们不需要实施破坏性更新(上述Pascal 的持久性参数)。


3
BSTs are much easier to implement in (pure) functional languages compared to hash-tables-真的吗?我想现在学习一种功能语言!
nawfal 2014年

1
哈希表需要以功能语言持久化。这通常会使实现复杂化。
我给予解答答案2014年

详细说来,如果您使用功能性语言来创建总裁数据结构,那么您最终真正要做的就是编写与汇编时相同的代码,除了在每个操作中您显式地转换内存/寄存器的数组或与服务器进行假装要做到这一点。所有人都是为了了解您的状态,但是如果正确完成的话,它与命令式方法是同构的(您无法现实地复制现实生活中每次转换的大量数据,您需要作弊)。
德米特里(Dmitry)'18

27

与哈希表相比,二叉树的主要优势在于,二叉树为您提供了哈希表无法(轻松,快速)执行的两个附加操作

  • 查找最接近(不一定等于)某个任意键值(或最接近上/下)的元素

  • 以排序的顺序遍历树的内容

两者是连接的-二叉树按排序顺序保留其内容,因此易于执行需要排序顺序的事情。


只有在不存在完全匹配的情况下,BST才能找到最接近的匹配,对吗?如果您发现根本身完全匹配怎么办?
developer747

2
@ developer747:然后在其上下的下一个最接近的是左侧子树的最右边的叶子和右侧子树的最左边的叶子。
克里斯·多德

16

(平衡的)二叉搜索树还具有以下优势:渐进复杂度实际上是一个上限,而哈希表的“恒定”时间是摊销时间:如果哈希函数不合适,最终可能会降级为线性时间,而不是常量。


3
为了说明这一点,当集合仅包含1个密钥的许多副本时,简并的情况就是。在BST中,插入为O(log n),在哈希表中,插入为O(n)
SingleNegationElimination

2
当哈希表仅包含1个键的许多副本时,insert是(仍然)O(1),而不是O(n)。哈希表的问题是当有许多具有相同哈希值的不同键时。这可以通过动态哈希方案来避免,该方案在发生许多冲突时切换到不同的哈希函数。
克里斯·多德

请注意,不平衡的树可以退化为列表,并且还具有O(n)查找。
awiebe '18

9

哈希表在首次创建时会占用更多空间-它将有可用的插槽供尚未插入的元素使用(无论是否插入过),二叉搜索树的大小只能满足需要是。另外,当散列表需要更多空间时,将其扩展到另一个结构可能会很耗时,但这可能取决于实现。


8

可以使用持久性接口来实现二叉搜索树,在该接口中返回新树,但旧树继续存在。精心实施后,新旧树共享它们的大部分节点。您不能使用标准哈希表执行此操作。


6

二叉树的搜索和插入速度较慢,但​​具有中缀遍历的非常好功能,这实际上意味着您可以按排序顺序遍历树的节点。

遍历哈希表的条目并没有多大意义,因为它们都分散在内存中。


6

摘自《破解编码访谈》,第六版

我们可以使用平衡二进制搜索树(BST)来实现哈希表。这为我们提供了O(log n)查找时间。这样做的好处是可以使用更少的空间,因为我们不再分配大型数组。我们还可以按顺序遍历键,有时这很有用。


5

BST还提供O(logn)时间中的“ findPredecessor”和“ findSuccessor”操作(以查找下一个最小和下一个最大的元素),这也可能是非常方便的操作。哈希表无法在该时间内提供效率。


如果要查找“ findPredecessor”和“ findSuccessor”操作,则HashTable首先是数据结构的错误选择。
AKDesai

1

如果要以排序方式访问数据,则必须与哈希表并行维护排序列表。一个很好的例子是.Net中的Dictionary。(请参阅http://msdn.microsoft.com/en-us/library/3fcwy8h6.aspx)。

这不仅会降低插入速度,而且会比b树消耗更多的内存。

此外,由于对b树进行了排序,因此很容易找到结果范围,或者执行并集或合并。


1

它还取决于使用情况,Hash允许定位精确匹配。如果要查询范围,则选择BST。假设您有很多数据e1,e2,e3 ..... en。

使用哈希表,您可以在恒定时间内定位任何元素。

如果要查找大于e41且小于e8的范围值,BST可以快速找到它。

关键是用于避免冲突的哈希函数。当然,我们不能完全避免冲突,在这种情况下,我们会采用链接或其他方法。这使得在最坏的情况下检索不再是恒定时间。

填充完毕后,哈希表必须增加其存储桶大小,并再次复制所有元素。这是BST所不具备的额外费用。


1

哈希表不适用于索引。搜索范围时,BST更好。这就是为什么大多数数据库索引使用B +树而不是哈希表的原因


数据库索引既是哈希树,又是B +树。当您想进行大于或小于的比较时,B +树索引很有用,否则哈希索引对查找很有用。还要考虑一下什么时候数据不具有可比性,并且如果您要创建索引,那么db将创建哈希索引,而不是B +树索引。@ssD
Sukhmeet Singh's

1

如果在键上定义了一些总顺序(键是可比较的),并且您想保留顺序信息,则二进制搜索树是实现字典的不错选择。

由于BST保留订单信息,因此它为您提供了四个附加的动态设置操作,这些操作无法使用哈希表(有效地)执行。这些操作是:

  1. 最大值
  2. 最低要求
  3. 接班人
  4. 前任

像每个BST操作一样,所有这些操作的时间复杂度均为O(H)。此外,所有存储的密钥仍在BST中排序,因此使您仅需按顺序遍历树即可获得排序的密钥序列。

总而言之,如果您只想执行操作插入,删除和删除操作,则哈希表在性能上(大多数时候)是无与伦比的。但是,如果要执行上面列出的任何或所有操作,则应使用BST,最好使用自平衡BST。


0

哈希表的主要优点是,它几乎可以完成〜= O(1)中的所有操作。而且它非常容易理解和实施。它确实有效地解决了许多“面试问题”。因此,如果您想破解编码面试,请使用哈希表结交最好的朋友;-)


我认为OP要求BST优于散列。
狙击手

0

哈希图是一个集合关联数组。因此,您的输入值数组将合并到存储桶中。在开放式编址方案中,您有一个指向存储桶的指针,并且每次向存储桶中添加新值时,都会发现存储桶中的可用空间。有几种方法可以执行此操作-从存储桶的开头开始,然后每次递增指针并测试其是否被占用。这称为线性探测。然后,您可以进行类似于add之类的二进制搜索,在其中您将存储桶开头之间的差值加倍,并且在每次搜索可用空间时将其加倍或递减。这称为二次探测。好。现在,这两种方法的问题在于,如果存储桶溢出到下一个存储桶地址,则您需要-

  1. 将每个存储桶的大小加倍-malloc(N个存储桶)/更改哈希函数-所需时间:取决于malloc的实现
  2. 将每个较早的存储桶数据传输/复制到新的存储桶数据。这是一个O(N)操作,其中N代表整个数据

好。但是,如果您使用链表,应该不会有这样的问题吧?是的,在链接列表中您没有这个问题。考虑到每个存储桶均以链接列表开头,并且如果存储桶中有100个元素,则需要遍历这100个元素以到达链接列表的末尾,因此List.add(Element E)将花费一些时间-

  1. 将元素散列到存储桶中-与所有实现一样,为Normal
  2. 花时间在所述存储桶-O(N)操作中找到最后一个元素。

链表实现的优点是,您不需要像开放式寻址实现那样对所有存储区进行内存分配操作和O(N)传输/复制。

因此,最小化O(N)操作的方法是将实现转换为二进制搜索树的实现,其中查找操作为O(log(N)),然后根据元素的值将元素添加到其位置。BST的附加功能是可以排序!


0

与字符串键一起使用时,二进制搜索树可以更快。特别是当字符串很长时。

二元搜索树使用较少/较大的比较,对于字符串而言比较快(当它们不相等时)。因此,当找不到字符串时,BST可以快速回答。找到后,只需要进行一次完整比较即可。

在哈希表中。您需要计算字符串的哈希值,这意味着您需要遍历所有字节至少一次以计算哈希值。然后,再次找到匹配的条目。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.