Answers:
堆仅保证较高级别的元素比较低级别的元素更大(最大堆)或更小(最小堆),而BST保证顺序(从“左”到“右”)。如果要排序元素,请使用BST。但丁写的不是怪胎
堆在findMin / findMax(O(1))上更好,而BST在所有发现上都很好(O(logN))。两种结构的插入均为O(logN)。如果只关心findMin / findMax(例如,优先级相关),请使用堆。如果您想对所有内容进行排序,请使用BST。
摘要
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)
摊销摊销(更强)的堆,例如斐波那契堆,甚至最坏的情况,例如Brodal队列,尽管由于非渐近性能可能不切实际:https : //stackoverflow.com/questions/30782636 在任何地方都可以使用/斐波那契堆或新娘队列
二进制堆可以在动态数组或基于指针的树(仅基于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来跟踪最大的元素并在可以更改该元素的任何时候对其进行更新都是微不足道的:插入较大的一个交换时,在删除时找到第二个最大的交换。https://stackoverflow.com/questions/7878622/can-we-use-binary-search-tree-to-simulate-heap-operation(由Yeo提及)。
实际上,与BST相比,这是堆的限制:唯一有效的搜索是对最大元素的搜索。
平均二进制堆插入为 O(1)
资料来源:
直观的论点:
在二进制堆中,增加给定索引的值也是O(1)
出于同样的原因。但是,如果您要这样做,则很可能希望使堆操作上的索引保持最新状态https://stackoverflow.com/questions/17009056/how-to-implement-ologn-decrease-基于最小堆的优先级队列的键操作,例如Dijkstra。无需额外时间即可实现。
GCC C ++标准库在真实硬件上插入基准
我对C ++ std::set
(红黑树BST)和std::priority_queue
(动态数组堆)插入进行了基准测试,以了解插入时间是否正确,这就是我得到的结果:
如此清楚:
堆插入时间基本上是恒定的。
我们可以清楚地看到动态数组调整大小点。由于我们平均每10k个插入片段就能看到高于系统噪声的所有信号,因此这些峰值实际上比所示的大10k倍!
缩放后的图实际上仅排除了数组调整大小点,并显示几乎所有插入都在25纳秒以下。
BST是对数的。所有插入都比平均堆插入慢得多。
有关BST与哈希图的详细分析,请访问:https://stackoverflow.com/questions/18414579/what-data-structure-is-inside-stdmap-in-c/51945119#51945119
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的确有不同的取舍。我还没有完全研究这个问题,但是最好在这里总结这些权衡:
也可以看看
关于CS的类似问题:二进制搜索树和二进制堆之间有什么区别?