BST和BST有什么区别?
何时使用堆以及何时使用BST?
如果要以排序的方式获取元素,那么BST是否比堆更好?
BST和BST有什么区别?
何时使用堆以及何时使用BST?
如果要以排序的方式获取元素,那么BST是否比堆更好?
Answers:
摘要
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)
资料来源:
直观的论点:
在二进制堆中,增加给定索引的值也是O(1)
出于同样的原因。但是,如果要执行此操作,则很可能希望使堆操作中的索引保持最新状态。如何为基于最小堆的优先级队列实现O(logn)减少键操作?例如Dijkstra。无需额外时间即可实现。
GCC C ++标准库在真实硬件上插入基准
我对C ++ std::set
(红黑树BST)和std::priority_queue
(动态数组堆)插入进行了基准测试,以查看插入时间是否正确,这就是我得到的:
如此清楚:
堆插入时间基本上是恒定的。
我们可以清楚地看到动态数组调整大小点。由于我们平均每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(n)
两者一个用例是当堆的键是当前时间戳时:在这种情况下,新条目将始终进入列表的开头。因此,我们甚至可以完全忘记确切的时间戳,而只将列表中的位置作为优先级即可。
这可以用来实现LRU缓存。就像对于Dijkstra这样的堆应用程序一样,您将需要保留从键到列表的相应节点的其他哈希图,以查找要快速更新的节点。
不同平衡BST的比较
尽管到目前为止,我所看到的通常归类为“平衡BST”的所有数据结构的渐近插入和查找时间是相同的,但不同的BBST确实有不同的取舍。我还没有完全研究这个问题,但是最好在这里总结这些权衡:
也可以看看
CS上的类似问题:https : //cs.stackexchange.com/questions/27860/whats-the-difference-between-a-binary-search-tree-and-a-binary-heap
堆仅保证较高级别的元素比较低级别的元素更大(最大堆)或更小(最小堆),而BST保证顺序(从“左”到“右”)。如果要排序元素,请使用BST。
[1, 5, 9, 7, 15, 10, 11]
表示有效的最小堆,但第7
3级的水平小于9
第2级的水平。有关可视化的信息,请参见示例Wikipedia图像中的25
和19
元素。(还请注意,元素之间的不平等关系并不严格,因为元素不一定是唯一的。)
何时使用堆以及何时使用BST
堆在findMin / findMax(O(1)
)上更好,而BST在所有发现(O(logN)
)上都很好。插入适用O(logN)
于两种结构。如果只关心findMin / findMax(例如,优先级相关),请使用堆。如果您想对所有内容进行排序,请使用BST。
这里的前几张幻灯片非常清楚地解释了事情。
如其他人提及的,堆可以做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模拟堆的更多讨论。
popMin
或popMax
不是O(1),而是O(log n),因为它必须是Balanced BST,每个删除操作都需要重新平衡。因此,它与二进制堆相同popMin
或popMax
运行O(log n)
二叉搜索树的定义是:对于每个节点,其左侧的节点具有较小的值(关键字),而其右侧的节点具有较大的值(关键字)。
作为二叉树的实现,作为堆使用以下定义:
如果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在堆上的另一种用法;由于存在重要差异:
在堆上使用BST:现在,让我们使用数据结构存储航班的着陆时间。如果着陆时间之差小于“ d”,我们将无法安排航班降落。并假设已安排许多航班降落在数据结构(BST或堆)中。
现在,我们要安排另一个将在t降落的航班。因此,我们需要计算t与它的后继者和前任者的差(应大于d)。 因此,我们将为此需要一个BST,它可以快速完成BST,即如果平衡的话可以在O(logn)中进行。
编辑:
BST 排序需要O(n)时间才能按排序顺序(顺序遍历)打印元素,而Heap可以在O(n logn)时间完成。堆提取min元素并重新堆整数组,使其在O(n logn)时间内进行排序。
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是公平的(如果毫无意义的话)。
堆仅保证较高级别的元素比较低级别的元素更大(最大堆)或更小(最小堆)
我喜欢上面的答案,并只针对我的需要和用法发表自己的评论。我必须获得n个位置列表,找到从每个位置到特定点的距离,例如(0,0),然后返回距离较小的am位置。我使用了堆优先级队列。为了找到距离并放入堆,每次插入都花了n(log(n))n个位置log(n)。然后为了获得最短距离的m,需要m(log(n))个m位置log(n)删除堆积的空间。
如果我必须使用BST进行此操作,那将使我陷入n(n)最坏的情况。(假设第一个值非常小,所有其他值依次变长,并且树只跨到右孩子或左孩子在越来越小的情况下,最小值将花费O(1)时间,但我又不得不进行平衡。因此,从我的情况和上述所有答案中,我得出的结论是,当您仅在以最小或最大优先级为基础的值开始变化时为堆。