堆与二进制搜索树(BST)

时间:2011-05-27 02:30:45

标签: algorithm binary-tree heap binary-search-tree

堆和BST有什么区别?

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

如果你想以排序的方式获取元素,BST是否优于堆?

8 个答案:

答案 0 :(得分:153)

<强>摘要

          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))是堆的杀手锏。

    Fibonacci Heap之外,还有其他堆已达O(1)摊销(更强),甚至最糟糕的情况,如Brodal queue,尽管它们可能不实用渐近性能:Are Fibonacci heaps or Brodal queues used in practice anywhere?

  • 二进制堆可以在dynamic arrays或基于指针的树之上有效地实现,BST只能在基于指针的树上实现。因此,对于堆,如果我们能够承受偶尔的调整大小延迟,我们可以选择更节省空间的数组实现。

  • BST的二进制堆创建is O(n) worst caseO(n log(n))

BST优于二进制堆的优势

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

    对于堆,一般来说它是O(n),除了最大的元素O(1)

<强>&#34;假&#34;堆超过BST的优势

  • O(1)找到最大值,BST O(log(n))

    这是一种常见的误解,因为修改BST以跟踪最大元素并在每次更改元素时更新它是微不足道的:在插入较大的一个交换时,在删除时找到第二大元素。 Can we use binary search tree to simulate heap operation?(提到by Yeo)。

    实际上,与BST相比,这是限制高效搜索是最大元素的搜索。

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

来源:

直观的论点:

  • 底层树的元素比顶层有更多的元素,所以新元素几乎肯定会在底部。
  • 堆插入starts from the bottom,BST必须从顶部开始

在二进制堆中,出于同样的原因,增加给定索引处的值也是O(1)。但是如果你想这样做,你很可能希望在堆操作How to implement O(logn) decrease-key operation for min-heap based Priority Queue?上保持最新的索引是最新的,例如为Dijkstra。可以在没有额外时间的情况下花费。

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

我使用Red-black tree BSTdynamic array heap对C ++ std::setthis code)和std::priority_queuethis plot script)插页进行基准测试,看看我是不是关于插入时间的权利,这就是我得到的:

enter image description here

如此清楚:

  • 堆插入时间基本上是不变的。

    我们可以清楚地看到动态数组调整大小点。由于我们平均每10k个插入to be able to see anything at all above system noise,这些峰值实际上比显示的大10k倍!

    缩放图基本上只排除了数组调整大小点,并显示几乎所有插入都落在25纳秒以下。

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

Ubuntu 18.04,GCC 7.3,Intel i7-7820HQ CPU,DDR4 2400 MHz RAM,联想Thinkpad P51。

GCC C ++标准库在gem5上插入基准

gem5是一个完整的系统模拟器,因此提供了一个带有m5 dumpstats的无限精确时钟。所以我试着用它来估算单个插页的时间。

enter image description here

解读:

  • 堆仍然是常量,但现在我们更详细地看到有几行,而且每一行都更稀疏。

    这必须对应于更高和更高插入的内存访问延迟。

  • TODO我无法完全解释BST,因为它看起来不那么对数而且更加稳定。

    有了这个更详细的细节,我们可以看到也可以看到一些不同的线条,但我不确定它们代表什么:我希望底线更薄,因为我们插入顶部底部?

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

无法在阵列上高效实施BST

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

保持BST平衡需要树旋转,这可以改变另一个的顶部元素,并且需要移动整个阵列(O(n))。

可以在数组上高效实现堆

可以从当前索引as shown here计算父子索引。

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

删除min是最令人担忧的操作,因为它必须自上而下。但它总是可以通过&#34;渗透&#34;堆的唯一分支as explained here。这导致了O(log(n))最坏的情况,因为堆总是很平衡。

如果您为每个删除的节点插入一个节点,那么您将失去堆积所提供的渐近O(1)平均插入的优势,因为删除将占主导地位,您也可以使用BST。然而,Dijkstra每次删除都会多次更新节点,所以我们没事。

动态数组堆与指针树堆

可以在指针堆上有效地实现堆:Is it possible to make efficient pointer-based binary heap implementations?

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

  • 树实现必须为每个元素存储三个指针:parent,left child和right child。所以内存使用总是4n(3个树指针+ 1 struct指针)。

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

  • 动态数组实现在加倍后可以是2n大小。所以平均来说它将是1.5n

另一方面,树堆具有更好的最坏情况插入,因为将后备动态数组复制到其大小加倍需要O(n)最坏情况,而树堆只为每个节点执行新的小分配。 / p>

尽管如此,后备阵列加倍是O(1)摊销的,因此它可以考虑到最大延迟时间。 Mentioned here

<强>哲学

  • BST在父级和所有后代之间维护一个全局属性(左侧更小,右侧更大)。

    BST的顶级节点是中间元素,需要全局知识来维护(知道有多少更小和更大的元素)。

    这个全局属性维护成本更高(log n insert),但提供更强大的搜索(log n search)。

  • 堆在父级和直接子级(父级&gt;子级)之间维护本地属性。

    堆的前调是大元素,只需要本地知识来维护(知道你的父母)。

双重链接列表

双链表可以看作是第一项具有最高优先级的堆的子集,所以我们也可以在这里比较它们:

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

用例就是当堆的密钥是当前时间戳时:在这种情况下,新条目将始终到达列表的开头。所以我们甚至可以完全忘记确切的时间戳,只需将列表中的位置作为优先级。

这可用于实现LRU cache。就像for heap applications like Dijkstra一样,您需要将密钥中的其他散列映射保留到列表的相应节点,以查找要快速更新的节点。

BST与哈希地图

详细分析:What data structure is inside std::map in C++?

这是预览:

enter image description here

另见

关于CS的类似问题:https://cs.stackexchange.com/questions/27860/whats-the-difference-between-a-binary-search-tree-and-a-binary-heap

答案 1 :(得分:68)

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

答案 2 :(得分:46)

  

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

堆在findMin / findMax(O(1))更好,而BST擅长所有找到(O(logN))。两个结构的插入均为O(logN)。如果您只关心findMin / findMax(例如与优先级相关),请使用堆。如果您想要排序所有内容,请使用BST。

来自here的前几张幻灯片非常清楚地解释了这一点。

答案 3 :(得分:7)

正如其他人所说,Heap可以在O(1)中执行findMin findMax,但不能在同一数据结构中执行。但是我不同意Heap在findMin / findMax中更好。事实上,经过稍作修改,BST可以在O(1)中执行 findMin findMax

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

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

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

更新

对类似问题Can we use binary search tree to simulate heap operation?的交叉引用,以获得有关使用BST模拟堆的更多讨论。

答案 4 :(得分:3)

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

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

如果A和B是节点,其中B是A的子节点,那么A的值(key)必须大于或等于B的值(key)。也就是说, 键(A)≥键(B)。

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

今天我为同一个问题跑了同样的问题而且我做对了。微笑...... :)

答案 5 :(得分:3)

BST在堆上的另一种用途;因为一个重要的区别:

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

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

现在,我们想安排另一架将降落在 t 的航班。因此,我们需要计算 t 与其后继者和前身的差异(应该> d)。 因此,我们需要一个BST,如果平衡的话,它会在O(logn)中快速

<强>编辑:

排序 BST需要花费O(n)时间按排序顺序打印元素(Inorder遍历),而Heap可以在O(n logn)时间内完成。 堆提取min元素并重新堆积数组,这使得它在O(n logn)时间内进行排序。

答案 6 :(得分:1)

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

答案 7 :(得分:0)

  

Heap只保证较高级别的元素(对于最大堆)或更小(对于最小堆)比较低级别的元素更高

我喜欢上述答案,并将我的评论更具体地说明了我的需求和用法。我必须得到n个位置列表找到从每个位置到特定点的距离说(0,0),然后返回距离较小的m个位置。我使用的是优先队列,即堆。 为了找到距离并放入堆中,每次插入都需要n(log(n))n个位置log(n)。 然后,为了获得具有最短距离的m,需要m(log(n))m个位置log(n)删除堆积。

我是否必须使用BST进行此操作,它会花费我n(n)最坏情况插入。(假设第一个值非常小,所有其他值依次越来越长,树只跨越到右边的孩子或者在小孩和小孩的情况下离开孩子。分钟将花费O(1)时间但是我必须平衡。 所以从我的情况和所有上面的答案我得到的是,当你只是在最小或最高优先级的基础上去堆之后。