堆vs二进制搜索树(BST)


169

BST和BST有什么区别?

何时使用堆以及何时使用BST?

如果要以排序的方式获取元素,那么BST是否比堆更好?


13
这个问题似乎离题,因为它与计算机科学有关,应在cs.stackexchange.com上提问
Flow


3
我觉得它与堆栈交换和堆栈溢出都有关。所以在这里拥有它很好
阿齐布罗

Answers:


191

摘要

          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的优势

  • O(1)对于BST 而言,平均插入二进制堆的时间为O(log(n))是堆的杀手feature。

    还有其他O(1)摊销摊销(更强)的,例如Fibonacci堆,甚至最坏的情况也是如此,例如Brodal队列,尽管由于非渐近性能它们可能不切实际:实际上在任何地方都使用了Fibonacci堆或Brodal队列吗?

  • 二进制堆可以在动态数组或基于指针的树(仅基于BST的指针树)之上有效实现。因此,对于堆来说,如果我们可以承受偶尔的调整大小延迟,则可以选择空间效率更高的数组实现。

  • 对于BST ,创建二进制堆O(n)最坏的情况O(n log(n))

BST相对于二进制堆的优势

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

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

堆相对于BST的“假”优势

  • 堆是O(1)查找最大值BST O(log(n))

    这是一个常见的误解,因为修改BST来跟踪最大的元素并在可以更改该元素的任何时候对其进行更新都是微不足道的:插入较大的一个交换时,在删除时找到第二个最大的交换。我们可以使用二叉搜索树来模拟堆操作吗?由Yeo提及)。

    实际上,与BST相比,这是堆的限制唯一有效的搜索是对最大元素的搜索。

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

资料来源:

直观的论点:

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

在二进制堆中,增加给定索引的值也是O(1)出于同样的原因。但是,如果要执行此操作,则很可能希望使堆操作中的索引保持最新状态。如何为基于最小堆的优先级队列实现O(logn)减少键操作?例如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)

如此清楚:

  • 堆插入时间基本上是恒定的。

    我们可以清楚地看到动态数组调整大小点。由于我们平均每10k个插入片段就能看到高于系统噪声的所有信号,因此这些峰值实际上比所示的大了约10k倍!

    缩放后的图实际上仅排除了数组调整大小点,并显示几乎所有插入都在25纳秒以下。

  • BST是对数的。所有插入都比平均堆插入慢得多。

  • BST与hashmap的详细分析在:C ++中std :: map内包含什么数据结构?

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每次删除都会多次更新节点,所以我们很好。

动态数组堆与指针树堆

可以在指针堆的顶部有效地实现堆:是否可以进行有效的基于指针的二进制堆实现?

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

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

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

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

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

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

哲学

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

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

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

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

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

比较BST,堆,哈希图:

  • BST:可以是以下任一种:

    • 无序集合(一种确定元素是否先前已插入的结构)。但是由于O(1)摊销后的插入,哈希图往往会更好。
    • 分拣机。但是堆通常会更好,这就是为什么排序树排序更广为人知的原因
  • 堆:只是分拣机。不能是有效的无序集合,因为您只能快速检查最小/最大的元素。

  • 哈希图:只能是无序集合,不能是高效的排序机,因为哈希会混淆任何排序。

双链表

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

  • 插入:
    • 位置:
      • 双向链表:插入的项必须是第一个或最后一个,因为我们只有指向那些元素的指针。
      • 二进制堆:插入的项目可以在任何位置结束。限制比链接列表少。
    • 时间:
      • 双链表: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上的类似问题:https : //cs.stackexchange.com/questions/27860/whats-the-difference-between-a-binary-search-tree-and-a-binary-heap


4
我+1了,但是证明平均O(1)二进制堆插入正当性的“纸”现在是一个死链接,而“幻灯片”仅陈述了没有证据的要求。另外,我认为这将有助于澄清“平均情况”是指假设插入值来自某个特定分布的平均值,因此我不确定此功能的真正“杀手级”。
j_random_hacker

3
BST和平衡的BST似乎可以互换使用。应该明确的是答案是平衡的BST,以避免混淆。
gkalpak

2
@Bulat我觉得我们有点偏离,但是如果我们同时想要max和min,那么如果我们不小心,可能会在维护两个堆时遇到问题-stackoverflow.com/a/1098454/7154924。使用最大最小堆(由于Atkinson等人)可能是更好的方法,这是专门为此目的设计的。
flow2k

1
@CiroSantilli新疆改造中心六四事件法轮功:我不明白为什么二进制堆的删除操作是O(log n)。仅当您具有指向堆中元素的指针时,此方法才有效,但是在大多数使用情况下,您具有键,并且需要首先找到采用O(n)的元素。
Ricola

5
堆插入是log(n)而不是o(1)
Bobo

78

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


8
“堆仅保证较高级别的元素比较低级别的元素更大(最大堆)或较小(最小堆),……” –堆不按级别强制执行此操作,而仅在父级-子执行链。[1, 5, 9, 7, 15, 10, 11]表示有效的最小堆,但第73级的水平小于9第2级的水平。有关可视化的信息,请参见示例Wikipedia图像中2519元素。(还请注意,元素之间的不平等关系并不严格,因为元素不一定是唯一的。)
Daniel Andersson

抱歉,迟到了,但我只是想弄清楚。如果对Binary Heap进行了排序,则搜索的最坏情况将是log n right。因此,在这种情况下,对Binary Heaps的排序要优于Binary Search Trees(红黑BST)。谢谢
克里希纳

50

何时使用堆以及何时使用BST

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

这里的前几张幻灯片非常清楚地解释了事情。


3
尽管在最坏的情况下,insert都是对数的,但平均堆插入需要固定的时间。(由于大多数现有元素都位于最下面,因此,在大多数情况下,即使有,新元素也只需冒出一两个层次。)
johncip 2014年

1
@xysun我认为BST在findMin和findMax中更好stackoverflow.com/a/27074221/764592
Yeo

2
@Yeo:堆对于findMin xor findMax 更好。如果两者都需要,那么BST更好。
Mooing Duck 2015年

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

1
西罗·桑蒂利(Ciro Santilli)的答案要好得多:stackoverflow.com/a/29548834/2873507
维克·西迪维尤

9

如其他人提及的,堆可以做findMin findMax在O(1)但不能同时在相同的数据结构。但是我不同意堆在findMin / findMax中更好。事实上,有轻微的变形例中,BST可以做 findMin findMax在O(1)。

在此经过修改的BST中,每次执行可能会修改数据结构的操作时,您都会跟踪最小节点和最大节点。例如,在插入操作中,您可以检查最小值是否大于新插入的值,然后将最小值分配给新添加的节点。可以将相同的技术应用于最大值。因此,此BST包含这些信息,您可以在O(1)中检索它们。(与二进制堆相同)

在此BST(平衡BST)中,当您pop min或时pop max,下一个要分配的最小值是min节点的后继者,而下一个要分配的最大值是max节点的前任。因此它在O(1)中执行。但是,我们需要重新平衡树,因此它仍将运行O(log n)。(与二进制堆相同)

希望在下面的评论中听到您的想法。谢谢 :)

更新资料

对类似问题的交叉引用我们可以使用二进制搜索树来模拟堆操作吗?有关使用BST模拟堆的更多讨论。


你为什么不同意?您愿意在下面分享您的想法吗?
2014年

您当然可以存储BST的最大值和/或最小值,但是如果要弹出BST会发生什么呢?您必须搜索树以将其删除,然后再次搜索新的最大/最小,这两个都是O(log n)操作。这与优先级堆中的插入和删除顺序相同,但常数更差。
贾斯汀2014年

@JustinLardinois对不起,我忘了在答案中强调这一点。在BST中,当您执行弹出min时,要分配的下一个min值是min节点的后继。如果弹出最大值,则下一个要分配的最大值是max节点的前身。因此它仍然在O(1)中执行。
Yeo 2014年

校正:for popMinpopMax不是O(1),而是O(log n),因为它必须是Balanced BST,每个删除操作都需要重新平衡。因此,它与二进制堆相同popMinpopMax运行O(log n)
Yeo

2
您可以获得第一个最小值/最大值,但是获得第k个最小值/最大值将回到正常的BST复杂度。
2015年

3

二叉搜索树的定义是:对于每个节点,其左侧的节点具有较小的值(关键字),而其右侧的节点具有较大的值(关键字)。

作为二叉树的实现,作为堆使用以下定义:

如果A和B是节点,其中B是A的子节点,则A的值(key)必须大于或等于B的值(key)。即key(A)≥key(B )。

http://wiki.answers.com/Q/Difference_between_binary_search_tree_and_heap_tree

今天我在考试中遇到了同样的问题,我做对了。微笑... :)


“堆,是二进制树的实现” –只是指出堆是一种二进制树,而不是BST
Saad 2015年

3

BST在堆上的另一种用法;由于存在重要差异:

  • 在BST中寻找继任者和前任者将花费O(h)时间。(平衡BST中的O(logn))
  • 而在堆中,将花费O(n)时间来查找某个元素的后继者或前任者。

在堆上使用BST:现在,让我们使用数据结构存储航班的着陆时间。如果着陆时间之差小于“ d”,我们将无法安排航班降落。并假设已安排许多航班降落在数据结构(BST或堆)中。

现在,我们要安排另一个将在t降落的航班。因此,我们需要计算t与它的后继者和前任者的差(应大于d)。 因此,我们将为此需要一个BST,它可以快速完成BST,如果平衡的话可以在O(logn)中进行。

编辑:

BST 排序需要O(n)时间才能按排序顺序(顺序遍历)打印元素,而Heap可以在O(n logn)时间完成。堆提取min元素并重新堆整数组,使其在O(n logn)时间内进行排序。


1
是。它是从未排序到已排序的序列。BST有序遍历的O(n)时间,它给出了排序的序列。在堆中时,您提取min元素,然后在O(log n)时间重新堆化。因此,将需要O(n logn)来提取n个元素。它将使您有一个排序的序列。
CODError 2015年

from unsorted to sorted sequence. O(n) time for inorder traversal of a BST, which gives sorted sequence.好吧,从未排序的序列到BST,我不知道一种基于键比较且时间少于O(n logn)的方法,该方法主导BST到序列的一部分。(尽管有O(n)堆构造。)。我认为状态堆接近未排序和已排序的BST是公平的(如果毫无意义的话)。
greybeard 2015年

我要在这里解释的是,如果您有一个BST以及一个n个元素的堆=>,那么所有元素都可以从两个数据结构中以有序的顺序打印,而BST可以在O(n)时间内完成(顺序遍历) ),而堆将花费O(n logn)时间。我不明白你想在这里说什么。How do you say BST将给你排序的顺序在O(n logn)。
CODError

我认为您也正在考虑花费一些时间来构建BST和堆。但是我假设您已经拥有了它,并且已经在一段时间内构建了它,现在您想要获得排序的结果。我不明白你的意思吗?
CODError 2015年

1
编辑...我希望您现在满意; p并提供+1(如果正确)。
CODError 2015年

1

将数组中的所有n个元素插入BST需要O(n logn)。可以在O(n)时间内将数组中的n个元素插入到堆中。这给了堆一定的优势


0

堆仅保证较高级别的元素比较低级别的元素更大(最大堆)或更小(最小堆)

我喜欢上面的答案,并只针对我的需要和用法发表自己的评论。我必须获得n个位置列表,找到从每个位置到特定点的距离,例如(0,0),然后返回距离较小的am位置。我使用了堆优先级队列。为了找到距离并放入堆,每次插入都花了n(log(n))n个位置log(n)。然后为了获得最短距离的m,需要m(log(n))个m位置log(n)删除堆积的空间。

如果我必须使用BST进行此操作,那将使我陷入n(n)最坏的情况。(假设第一个值非常小,所有其他值依次变长,并且树只跨到右孩子或左孩子在越来越小的情况下,最小值将花费O(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.