二进制搜索树和二进制堆之间有什么区别?


Answers:


63

堆仅保证较高级别的元素比较低级别的元素更大(最大堆)或更小(最小堆),而BST保证顺序(从“左”到“右”)。如果要排序元素,请使用BST。但丁写的不是怪胎

堆在findMin / findMax(O(1))上更好,而BST在所有发现上都很好(O(logN))。两种结构的插入均为O(logN)。如果只关心findMin / findMax(例如,优先级相关),请使用堆。如果您想对所有内容进行排序,请使用BST。

由xysun


我认为,BST是findMin&findMax更好stackoverflow.com/a/27074221/764592

10
我认为这只是一个普遍的误解。可以很容易地修改二叉树以找到Yeo指出的最小值和最大值。这实际上是对堆的限制唯一有效的查找是min或max。正如我所解释的,堆的真正优势是O(1)平均插入stackoverflow.com/a/29548834/895245
Ciro Santilli新疆改造中心法轮功六四事件

根据此视频,您可以在较低级别上使用较大的值,只要较大的值不是较低级别的后代即可。
whoan

堆从根到叶排序,BST从左到右排序。
Deep Joshi

34

两个二叉搜索树二叉堆是基于树的数据结构。

堆要求节点优先于其子节点。在最大堆中,每个节点的子节点必须小于自身。最小堆的情况与此相反:

二进制最大堆

二进制搜索树(BST)在兄弟节点之间遵循特定的顺序(预顺序,顺序,后顺序)。与堆不同,必须对树进行排序:

二进制搜索树

O(logn)
O(1)O(logn)


1
O(logn)

32

摘要

          Type      BST (*)   Heap
Insert    average   log(n)    1
Insert    worst     log(n)    log(n) or n (***)
Find any  worst     log(n)    n
Find max  worst     1 (**)    1
Create    worst     n log(n)  n
Delete    worst     log(n)    log(n)

该表上的所有平均时间都与最差时间相同,除了插入。

  • *:在此答案中到处都是BST ==平衡的BST,因为不平衡的渐近吸
  • **:使用此答案中说明的简单修改
  • ***log(n)对于指针树堆,n对于动态数组堆

二进制堆比BST的优势

BST相对于二进制堆的优势

  • 搜索任意元素是O(log(n))是BST的杀手级功能。

    对于堆来说,通常是这样O(n),除了最大的元素是O(1)

堆相对于BST的“假”优势

平均二进制堆插入为 O(1)

资料来源:

直观的论点:

  • 底层树的元素数量比顶层树的数量多,因此几乎可以肯定,新元素将排在底层
  • 堆插入从底部开始,BST必须从顶部开始

在二进制堆中,增加给定索引的值也是O(1)出于同样的原因。但是,如果您要这样做,则很可能希望使堆操作上的索引保持最新状态https://stackoverflow.com/questions/17009056/how-to-implement-ologn-decrease-基于最小堆的优先级队列的键操作,例如Dijkstra。无需额外时间即可实现。

GCC C ++标准库在真实硬件上插入基准

我对C ++ std::set红黑树BST)和std::priority_queue动态数组堆)插入进行了基准测试,以了解插入时间是否正确,这就是我得到的结果:

在此处输入图片说明

  • 基准代码
  • 剧情脚本
  • 绘图数据
  • 在Ubuntu 19.04,GCC 8.3.0上的Lenovo ThinkPad P51笔记本电脑上进行了测试,配备CPU:Intel Core i7-7820HQ CPU(4核心/ 8线程,2.90 GHz基本,8 MB缓存),RAM:2x三星M471A2K43BB1-CRC(2x 16GiB ,2400 Mbps),SSD:三星MZVLB512HAJQ-000L7(512GB,3,000 MB / s)

如此清楚:

Gem5上的GCC C ++标准库插入基准

gem5是一个完整的系统模拟器,因此通过提供了一个无限精确的时钟m5 dumpstats。因此,我尝试使用它来估计单个插入的时间。

在此处输入图片说明

解释:

  • 堆仍然是不变的,但是现在我们更详细地看到有几行,而每行越高则越稀疏。

    这必须与为越来越多的插入操作完成的内存访问延迟相对应。

  • TODO我不能真正地完全解释BST,因为它看起来不是对数的,而且有点常数。

    但是,有了这个更详细的信息,我们还可以看到一些不同的线,但是我不确定它们代表什么:我希望底线会更细些,因为我们插入了顶底?

在aarch64 HPI CPU上使用此Buildroot 设置进行了基准测试。

无法在阵列上有效实施BST

堆操作只需要使单个树枝向上或向下冒泡,因此,O(log(n))最坏情况下的交换O(1)平均。

要使BST保持平衡,就需要旋转树,这可能会更改顶部元素的另一个元素,并且需要在(O(n))周围移动整个数组。

堆可以在阵列上有效实现

可以从当前索引计算父级和子级索引,如下所示

没有像BST这样的平衡操作。

删除最小值是最令人担忧的操作,因为它必须自上而下。但它总是可以通过“向下渗透”堆单支做如下解释。这导致O(log(n))最坏的情况,因为堆总是平衡良好。

如果为要删除的每个节点插入一个节点,那么您将失去堆提供的渐近O(1)平均插入的优势,因为删除将占主导地位,并且您最好使用BST。但是,Dijkstra每次删除都会多次更新节点,所以我们很好。

动态数组堆与指针树堆

可以在指针堆之上有效地实现堆:https : //stackoverflow.com/questions/19720438/is-it-possible-to-make-efficiency-pointer-based-binary-heap-implementations

动态数组实现更节省空间。假设每个堆元素仅包含一个指向的指针struct

  • 树实现必须为每个元素存储三个指针:父,左子和右子。因此,内存使用率始终为4n(3个树指针+1个struct指针)。

    树型BST还需要进一步的平衡信息,例如黑红色。

  • 动态数组实现的大小2n可以在加倍后立即确定。因此,平均而言1.5n

另一方面,树堆具有更好的最坏情况插入条件,因为将后备动态数组复制为其大小的两倍会O(n)导致最坏情况,而树堆只是为每个节点进行新的小分配。

尽管如此,支持阵列的倍增仍要O(1)摊销,因此归结为最大的延迟考虑因素。这里提到

哲学

  • BST在父级和所有后代之间保持全局属性(左较小,右较大)。

    BST的顶层节点是中间元素,它需要全球知识来维护(知道那里有多少个较小和较大的元素)。

    此全局属性的维护成本更高(登录n插入),但提供更强大的搜索(登录n搜索)。

  • 堆在父级和直子(父级>子级)之间保持局部属性。

    堆的顶部是大元素,它只需要本地知识即可维护(了解您的父母)。

双链表

双向链表可以看作是第一项具有最高优先级的堆的子集,因此让我们在这里进行比较:

  • 插入:
    • 位置:
      • 双向链表:插入的项必须是第一个或最后一个,因为我们只有指向那些元素的指针。
      • 二进制堆:插入的项目可以放在任何位置。限制比链接列表少。
    • 时间:
      • 双链表:O(1)最坏的情况,因为我们有指向项目的指针,并且更新非常简单
      • 二进制堆:O(1)平均,因此比链表差。权衡具有更一般的插入位置。
  • 搜索:O(n)两者

一个用例是堆的键是当前时间戳记:在这种情况下,新条目将始终进入列表的开头。因此,我们甚至可以完全忘记确切的时间戳,只将列表中的位置保留为优先。

这可以用来实现LRU缓存。就像对于Dijkstra这样的堆应用程序一样,您将需要保留从键到列表的相应节点的其他哈希图,以查找要快速更新的节点。

不同平衡BST的比较

尽管到目前为止,我所看到的通常归类为“平衡BST”的所有数据结构的渐近插入和查找时间是相同的,但是不同的BBST的确有不同的取舍。我还没有完全研究这个问题,但是最好在这里总结这些权衡:

  • 红黑树。似乎是截至2019年最常用的BBST,例如,它是GCC 8.3.0 C ++实现使用的一种
  • AVL树。似乎比BST更加平衡,因此查找延迟可能会更好,但代价是查找代价稍高一些。Wiki总结:“ AVL树通常与红黑树进行比较,因为它们都支持相同的操作集,并且需要[相同]的时间进行基本操作。对于查找密集型应用程序,AVL树比红黑树要快,因为类似于红黑树,AVL树是高度平衡的,通常,对于任何<1/2的亩,它们既没有重量平衡也没有mu平衡;也就是说,同级节点可以具有巨大的平衡后代数量不同。”
  • WAVL。在原始论文中提到在再平衡和旋转操作范围方面该版本的优点。

也可以看看

关于CS的类似问题:二进制搜索树和二进制堆之间有什么区别?


1
好答案。堆的常见应用是中位数,k min,前k个元素。对于这些最常见的操作,先删除min然后再插入(通常,我们的堆很小,几乎没有纯插入操作)。在实践中,对于这些算法,它的性能似乎不比BST好。
yura

1
出色的答案!!!通过使用双端队列作为基础堆结构,尽管它仍然是O(n)最坏的情况,因为它需要重新分配(较小的)指向块的指针数组,所以可以大大减少调整大小的时间。
布拉特

13

对于数据结构,必须区分关注级别。

  1. 这个问题中的抽象数据结构(存储的对象,它们的操作)是不同的。一个实现优先级队列,另一个实现优先级队列。优先级队列对查找任意元素不感兴趣,仅查找优先级最高的元素。

  2. 结构的具体实现。乍一看,它们都是(二叉树),但是具有不同的结构特性。键的相对顺序和可能的全局结构都不同。(有些不精确,在BST键中,键是从左到右排列的,在堆中,它们是自上而下排列的。)IPlant正确地指出,堆也应该是“完整的”。

  3. 低层次的实现中有最终的区别。(不平衡)二进制搜索树具有使用指针的标准实现。相反,二进制堆具有使用数组的有效实现(正是由于结构受限制)。


1

除了前面的答案之外,堆还必须具有堆结构属性。树必须是满的,最底下的层(不一定总是满的)必须从最左到最右无间隙地填充。

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.