二进制搜索树相对于哈希表有什么优势?
哈希表可以查找Theta(1)时间内的任何元素,并且添加元素也同样容易....但我不确定反过来的优势。
答案 0 :(得分:110)
其他人没有指出的一个优点是二叉搜索树允许您有效地进行范围搜索。
为了说明我的想法,我想提出一个极端的例子。假设您想获得其键在0到5000之间的所有元素。实际上只有一个这样的元素和10000个其他元素的键不在范围内。 BST可以非常有效地进行范围搜索,因为它不搜索不可能得到答案的子树。
虽然,如何在哈希表中进行范围搜索?您需要迭代每个存储区空间,即O(n),或者您必须查找1,2,3,4 ...高达5000中的每一个是否存在。 (0到5000之间的键是一个无限集?例如键可以是小数)
答案 1 :(得分:84)
请记住,二进制搜索树(基于参考)是内存有效的。他们没有保留比他们需要更多的内存。
例如,如果散列函数的范围为R(h) = 0...100
,则需要分配一个包含100个(指针)元素的数组,即使您只是散列20个元素。如果您使用二叉搜索树来存储相同的信息,您只需分配所需的空间,以及有关链接的一些元数据。
答案 2 :(得分:74)
二叉树的一个“优点”是可以遍历它以按顺序列出所有元素。使用Hash表并不是不可能的,但是将其设计为散列结构的正常操作。
答案 3 :(得分:50)
除了所有其他好评:
与二叉树相比,散列表通常具有更好的缓存行为,需要更少的内存读取。对于哈希表,在访问包含数据的引用之前,通常只会发生一次读取。二叉树,如果它是一个平衡变量,需要以 k * lg(n)的顺序排列某些常数k的内存。
另一方面,如果敌人知道您的哈希函数,则敌人可以强制执行您的哈希表以进行冲突,从而严重影响其性能。解决方法是从一个系列中随机选择哈希函数,但是BST没有这个缺点。此外,当哈希表压力增长太多时,您经常倾向于放大并重新分配哈希表,这可能是一个昂贵的操作。 BST在这里具有更简单的行为,并且不会突然分配大量数据并进行重新运算。
树往往是最终的平均数据结构。它们可以充当列表,可以轻松拆分以进行并行操作,快速删除,插入和查找 O(lg n)。他们没有做任何事情特别,但他们也没有任何过分恶劣的行为。
最后,与哈希表相比,BST在(纯)函数语言中更容易实现,并且它们不需要实现破坏性更新(Pascal上面的持久性参数)。
答案 4 :(得分:26)
二进制树优于哈希表的主要优点是二叉树为您提供了两个额外的操作(使用哈希表无法轻松快速地执行)
找到最接近(不一定等于)某个任意键值(或最接近上/下)的元素
按排序顺序遍历树的内容
这两个是连接的 - 二叉树以排序顺序保存其内容,因此需要排序顺序的东西很容易做到。
答案 5 :(得分:15)
(平衡)二叉搜索树的优点还在于其渐近复杂度实际上是一个上限,而哈希表的“常量”时间是分摊的时间:如果你有一个不合适的哈希函数,你最终可能会降级线性时间,而不是常数。
答案 6 :(得分:9)
哈希表在首次创建时会占用更多空间 - 它将为尚未插入的元素提供可用的插槽(无论是否插入它们),二进制搜索树只会像以前一样大它需要。此外,当哈希表需要更多空间时,扩展到另一个结构可能非常耗时,但这可能取决于实现。
答案 7 :(得分:8)
二进制搜索树可以使用持久性接口实现,其中返回新树但旧树继续存在。仔细实施,新旧树共享大部分节点。您无法使用标准哈希表执行此操作。
答案 8 :(得分:6)
二叉树搜索和插入速度较慢,但具有中缀遍历的非常好的功能,这实际上意味着您可以按排序顺序遍历树的节点。
迭代哈希表的条目并没有多大意义,因为它们都散布在内存中。
答案 9 :(得分:4)
来自Cracking the Coding Interview, 6th Edition
我们可以使用平衡二叉搜索树(BST)实现哈希表。这为我们提供了O(log n)查找时间。这样做的好处是可能占用更少的空间,因为我们不再分配大型数组。我们也可以按顺序遍历键,这有时很有用。
答案 10 :(得分:4)
BST还在O(logn)时间内提供“findPredecessor”和“findSuccessor”操作(以查找下一个最小和下一个最大的元素),这也可能是非常方便的操作。哈希表不能提供那段时间的效率。
答案 11 :(得分:1)
GCC C++ 案例研究
让我们也从世界上最重要的实现之一中获得一些见解。正如我们将要看到的,它实际上与理论完美匹配!
如 What is the underlying data structure of a STL set in C++? 所示,在 GCC 6.4 中:
std::map
使用 BSTstd::unordered_map
使用哈希图所以这已经指出了一个事实,即您无法有效地遍历哈希图,这可能是 BST 的主要优势。
然后,我还在 Heap vs Binary Search Tree (BST) 处对哈希映射、BST 和堆中的插入时间进行了基准测试,这清楚地突出了关键性能特征:
BST 插入是 O(log),hashmap 是 O(1)。在这个特定的实现中,hashmap 几乎总是比 BST 快,即使是相对较小的尺寸
hashmap 虽然总体上要快得多,但有一些非常缓慢的插入,在缩小的图中显示为单个点。
当实现决定是时候增加其大小并且需要将其复制到更大的大小时,就会发生这种情况。
更准确地说,这是因为只有它的 amortized complexity 是 O(1),而不是最坏的情况,在数组复制期间实际上是 O(n)。
这可能会使哈希图不适用于某些需要更强时间保证的实时应用程序。
相关:
答案 12 :(得分:1)
它还取决于使用,Hash允许定位完全匹配。如果要查询范围,则可以选择BST。假设您有大量数据e1,e2,e3 ..... en。
使用哈希表,您可以在恒定时间内找到任何元素。
如果你想找到大于e41且小于e8的范围值,BST可以很快找到它。
关键是用于避免冲突的哈希函数。当然,我们不能完全避免碰撞,在这种情况下我们采用链接或其他方法。这使得在最坏的情况下检索不再是恒定的时间。
一旦填满,哈希表必须增加其桶大小并再次复制所有元素。这是BST没有的额外费用。
答案 13 :(得分:1)
如果要以排序方式访问数据,则必须与哈希表并行维护排序列表。一个很好的例子是.Net中的Dictionary。 (见http://msdn.microsoft.com/en-us/library/3fcwy8h6.aspx)。
这样做的副作用不仅是减慢插入速度,而且比b树消耗更多的内存。
此外,由于对b树进行了排序,因此很容易找到结果范围,或执行联合或合并。
答案 14 :(得分:0)
哈希表不适合编制索引。当您搜索范围时,BST更好。这就是大多数数据库索引使用B +树而不是哈希表的原因
答案 15 :(得分:0)
散列映射是一组关联数组。因此,您的输入值数组会汇集到存储桶中。在开放式寻址方案中,您有一个指向存储桶的指针,每次向存储桶添加新值时,都会发现存储桶中有空闲空间的位置。有几种方法可以做到这一点 - 你从桶的开头开始并每次递增指针并测试它是否被占用。这称为线性探测。然后,您可以执行二进制搜索,例如添加,您可以在每次搜索可用空间时将存储桶开头与加倍或缩减之间的差异加倍。这称为二次探测。 好。现在这两种方法的问题是如果桶溢出到下一个桶地址,那么你需要 -
行。但如果你使用链表,不应该有这样的问题吗?是的,在链接列表中,您没有此问题。考虑每个存储桶以链表开头,如果存储桶中有100个元素,则需要遍历这100个元素才能到达链表的末尾,因此List.add(元素E)需要时间 -
链表实现的优点是您不需要像开放寻址实现那样的内存分配操作和所有存储桶的O(N)传输/复制。
因此,最小化O(N)操作的方法是将实现转换为二进制搜索树的实现,其中查找操作为O(log(N)),并根据其值将元素添加到其位置。 BST的附加功能是排序!
答案 16 :(得分:0)
二进制搜索树是实现字典的好选择,如果密钥在它们上定义了一些总顺序(密钥是可比较的)并且您想要保留订单信息。
由于BST保留了订单信息,因此它为您提供了四个额外的动态集操作,这些操作无法使用哈希表(高效地)执行。这些操作是:
所有这些操作,如每个BST操作都具有O(H)的时间复杂度。此外,所有存储的密钥仍然在BST中排序,因此您只需按顺序遍历树即可获得已排序的密钥序列。
总之,如果你想要的只是操作插入,删除和删除,那么哈希表在性能上是无与伦比的(大部分时间)。但是如果你想要上面列出的任何或所有操作,你应该使用BST,最好是自平衡BST。
答案 17 :(得分:0)
与字符串键一起使用时,二进制搜索树可以更快。尤其是当字符串很长时。
使用较少/较大的比较的二分搜索树,对于字符串来说比较快(当它们不相等时)。因此,当找不到字符串时,BST可以快速回答。 找到后,只需要进行一次完整比较即可。
在哈希表中。您需要计算字符串的哈希值,这意味着您需要遍历所有字节至少一次以计算哈希值。然后,再次找到匹配的条目。
答案 18 :(得分:-1)