【问题标题】:Advantages of Binary Search Trees over Hash Tables二叉搜索树相对于哈希表的优势
【发布时间】:2011-05-06 22:13:12
【问题描述】:

二叉搜索树相对于哈希表的优势是什么?

哈希表可以在 Theta(1) 时间内查找任何元素,并且添加元素同样容易......但我不确定反过来的优势。

【问题讨论】:

  • 对于哈希表 find() insert() 和 remove() 的运行时间是多少? theta(1) theta(1) 和 theta(1) 对吗?
  • 几乎总是,是的。如果你遇到很多碰撞,那么这些时间可能会增长到 O(n)。
  • 这些时间还取决于您的散列函数。如果由于某种奇怪的原因它不是 O(1),那么显然您的操作将具有您的哈希函数运行效率的最低限度。
  • 我想说 BST 的最大优点是它是一个排序的数据结构。详细用例已经列出here

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


【解决方案1】:

没有人指出的一个优点是二叉搜索树允许您有效地进行范围搜索。

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

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

【讨论】:

  • BST 高效地进行范围搜索!对我来说,这是实用和算法方法的最佳答案。
  • 哇,这真的解释了为什么树与数据库如此相关;当您需要执行基于密钥的过滤时,它们的好处最为明显。使用哈希映射,您需要遍历所有键以解决“查找键在 1000 到 3290 之间的所有项目”
【解决方案2】:

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

例如,如果哈希函数的范围为R(h) = 0...100,那么您需要分配一个包含 100 个(指向)元素的数组,即使您只是对 20 个元素进行哈希处理。如果您要使用二叉搜索树来存储相同的信息,您将只分配所需的空间以及一些有关链接的元数据。

【讨论】:

  • 哈希函数输出的整个范围必须存在于数组中是不正确的。哈希值可以简单地通过数组的长度进行修改以允许更小的数组。当然,最终添加的元素数量可能是未知的,因此哈希表可能仍会分配比必要更多的空间。不过,二叉搜索树可能会浪费同样多的内存或更多。链接的实现需要为每个元素提供至少两个额外指针的空间(如果使用父指针,则需要三个),并且基于数组的 BST 可能会为树的未填充部分浪费大量内存。
  • @Solaraeus:基于数组的 BST 与哈希表相比是最好的,而且它们并不比哈希表更浪费。与重新计算整个表相比,您还可以仅使用内存副本扩展 BST。
【解决方案3】:

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

【讨论】:

【解决方案4】:

除了所有其他优秀的 cmets:

与二叉树相比,哈希表通常具有更好的缓存行为,需要更少的内存读取。对于哈希表,您通常只需要进行一次读取,然后才能访问保存数据的引用。二叉树,如果它是一个平衡的变体,则需要按 k * lg(n) 顺序进行内存读取以获取某个常数 k。

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

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

最后,与哈希表相比,BST 在(纯)函数式语言中更容易实现,并且它们不需要执行破坏性更新(上面 Pascal 的 persistence 参数)。

【讨论】:

  • BSTs are much easier to implement in (pure) functional languages compared to hash-tables - 真的吗?我现在想学习一门函数式语言!
  • 哈希表需要在函数式语言中持久化。这通常会使实现复杂化。
  • 详细说明,如果您使用函数式语言制作总裁数据结构,那么您最终所做的就是编写与汇编中相同的代码,除了在每个操作中您显式地转换您的内存/寄存器数组,或与服务器交谈以假装这样做。我完全是为了了解您的状态,但如果操作正确,它与命令式方法同构(您无法在现实生活中实际复制大量数据以进行每次转换,您需要作弊)。
【解决方案5】:

与哈希表相比,二叉树的主要优势在于二叉树为您提供了两个使用哈希表无法(轻松、快速)完成的额外操作

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

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

两者是相互联系的——二叉树将其内容保持在排序顺序中,因此需要排序顺序的事情很容易做到。

【讨论】:

  • BST 会找到最接近的匹配项,但前提是不存在完全匹配项,对吧?如果您在根本身找到完全匹配怎么办?
  • @developer747:那么下面和上面最接近的是左子树的最右边的叶子和右子树的最左边的叶子。
【解决方案6】:

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

【讨论】:

  • 为了说明这一点,一个退化的情况是当集合包含许多副本时只有一个密钥。在 BST 中,insert 是 O(log n),在 Hash 表中,insert 是 O(n)
  • 当哈希表只包含一个键的多个副本时,插入(仍然)是 O(1),而不是 O(n)。哈希表的问题是当有许多 不同的 键具有相同的哈希时。这可以通过动态散列方案来避免,该方案在发生许多冲突时切换到不同的散列函数。
  • 注意,一个不平衡的树可以退化成一个列表并且也有 O(n) 查找。
【解决方案7】:

哈希表在第一次创建时会占用更多空间——它将为尚未插入的元素提供可用的插槽(无论它们是否曾经插入),二叉搜索树将只有它需要。此外,当哈希表需要更多空间时,扩展到另一个结构可能会很耗时,但这可能取决于实现。

【讨论】:

    【解决方案8】:

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

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

    【讨论】:

      【解决方案9】:

      二叉搜索树可以通过persistent接口实现,其中返回新树但旧树继续存在。仔细实施后,新旧树共享它们的大部分节点。使用标准哈希表无法做到这一点。

      【讨论】:

        【解决方案10】:

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

        【讨论】:

        • 如果你正在寻找“findPredecessor”和“findSuccessor”操作,那么HashTable首先是一个糟糕的数据结构选择。
        【解决方案11】:

        来自Cracking the Coding Interview, 6th Edition

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

        【讨论】:

          【解决方案12】:

          GCC C++ 案例研究

          让我们也从世界上最重要的实现之一中获得一些见解。正如我们将看到的,它实际上与理论完美匹配!

          What is the underlying data structure of a STL set in C++? 所示,在 GCC 6.4 中:

          • std::map 使用 BST
          • std::unordered_map 使用 hashmap

          所以这已经指出了一个事实,即您不能有效地遍历哈希图,这可能是 BST 的主要优势。

          然后,我还在Heap vs Binary Search Tree (BST) 对哈希映射、BST 和堆中的插入时间进行了基准测试,这清楚地突出了关键性能特征:

          • BST 插入是 O(log),hashmap 是 O(1)。在这个特定的实现中,hashmap 几乎总是比 BST 快,即使是相对较小的尺寸

          • hashmap 虽然总体上要快得多,但在缩小的图中显示为单个点的一些极慢的插入。

            当实现决定是时候增加其大小并且需要将其复制到更大的大小时,就会发生这种情况。

            更准确地说,这是因为只有它的amortized complexity是O(1),而不是最坏的情况,在数组复制过程中实际上是O(n)。

            这可能会使哈希图不适用于某些需要更强时间保证的实时应用程序。

          相关:

          【讨论】:

            【解决方案13】:

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

            这不仅会减慢插入速度,而且会比 b-tree 消耗更多的内存。

            此外,由于 b 树已排序,因此查找结果范围或执行联合或合并很简单。

            【讨论】:

              【解决方案14】:

              这也取决于用途,哈希允许定位精确匹配。如果你想查询一个范围,那么 BST 是选择。假设你有很多数据 e1, e2, e3 ..... en.

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

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

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

              一旦填满,哈希表必须增加其存储桶大小并再次复制所有元素。这是 BST 不存在的额外费用。

              【讨论】:

                【解决方案15】:

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

                由于 BST 保留了订单信息,它为您提供了四种无法(有效)使用哈希表执行的动态集合操作。这些操作是:

                1. 最大值
                2. 最低
                3. 继任者
                4. 前任

                所有这些操作就像每个 BST 操作一样具有 O(H) 的时间复杂度。此外,所有存储的键在 BST 中保持排序,因此您只需按顺序遍历树即可获得排序的键序列。

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

                【讨论】:

                  【解决方案16】:

                  hashmap 是一个集合关联数组。因此,您的输入值数组被汇集到存储桶中。在开放式寻址方案中,您有一个指向存储桶的指针,每次您向存储桶添加新值时,您都会发现存储桶中的哪些位置有空闲空间。有几种方法可以做到这一点——你从桶的开头开始,每次都增加指针并测试它是否被占用。这称为线性探测。然后,您可以进行像 add 一样的二分搜索,在每次搜索可用空间时,将存储桶的开头与加倍或后退之间的差加倍。这称为二次探测。 好的。现在这两种方法的问题是,如果桶溢出到下一个桶地址,那么你需要-

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

                  好的。但是如果你使用链表就不应该有这样的问题,对吧?是的,在链表中你没有这个问题。考虑到每个桶都以链表开头,如果桶中有 100 个元素,则需要遍历这 100 个元素才能到达链表的末尾,因此 List.add(Element E) 需要时间-

                  1. 将元素散列到一个桶中——在所有实现中都是正常的
                  2. 花时间找到所述桶中的最后一个元素 - O(N) 操作。

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

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

                  【讨论】:

                    【解决方案17】:

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

                    【讨论】:

                    • 数据库索引是哈希树和 B+ 树的两种类型。当您想进行大于或小于之类的比较时,B+ 树索引很有用,否则哈希索引对查找很有用。还要考虑什么时候数据不可比较,如果你想创建索引,那么 db 将创建哈希索引而不是 B+ 树索引。 @ssD
                    • 您能否提供“更好”声明的来源?
                    【解决方案18】:

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

                    二叉搜索树使用较小/较大的比较,这对于字符串来说很快(当它们不相等时)。因此,当找不到字符串时,BST 可以快速回答。 找到后,只需进行一次完整比较。

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

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2011-01-03
                      • 2017-11-23
                      • 1970-01-01
                      • 2014-01-23
                      • 2010-10-25
                      • 2018-05-11
                      • 2010-12-06
                      • 2020-04-20
                      相关资源
                      最近更新 更多