堆和BST有什么区别?
何时使用堆以及何时使用BST?
如果你想以排序的方式获取元素,BST是否优于堆?
答案 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 case,O(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)
来源:
直观的论点:
在二进制堆中,出于同样的原因,增加给定索引处的值也是O(1)
。但是如果你想这样做,你很可能希望在堆操作How to implement O(logn) decrease-key operation for min-heap based Priority Queue?上保持最新的索引是最新的,例如为Dijkstra。可以在没有额外时间的情况下花费。
GCC C ++标准库在真实硬件上插入基准
我使用Red-black tree BST和dynamic array heap对C ++ std::set
(this code)和std::priority_queue
(this plot script)插页进行基准测试,看看我是不是关于插入时间的权利,这就是我得到的:
如此清楚:
堆插入时间基本上是不变的。
我们可以清楚地看到动态数组调整大小点。由于我们平均每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
的无限精确时钟。所以我试着用它来估算单个插页的时间。
解读:
堆仍然是常量,但现在我们更详细地看到有几行,而且每一行都更稀疏。
这必须对应于更高和更高插入的内存访问延迟。
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++?
这是预览:
另见
答案 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 min
或pop 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 :现在,让我们说我们使用数据结构来存储航班的着陆时间。如果着陆时间差异小于“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)时间但是我必须平衡。 所以从我的情况和所有上面的答案我得到的是,当你只是在最小或最高优先级的基础上去堆之后。