二叉堆操作时间复杂度分析

转载自:https://blog.csdn.net/jmh1996/article/details/83662953

本blog主要介绍了二叉堆、二项式堆,下一篇博客将介绍斐波拉契堆。

二叉堆和二项式堆、斐波拉契堆都是用于实现优先队列的高级数据结构,以不同堆实现的优先队列会有不同的时间复杂度。

问题引入
在实际应用中,我们经常会遇到在最多由n个数组成的动态集合S SS上得到这个集合里面的最大值或者最小值。这里的动态是指:集合S里面的元素可能会随时增加、删除、修改、返回最小值、返回最小值并删除一个最小值。

我们把用于解决此类的问题的抽象数据结构定义为优先队列:priority queue.之所以叫优先队列是指里面的元素都是具有偏序关系的、也就是可以比较大小的。
priority_queue有以下几种基本操作:

insert(H,x) 插入一个值域为x的元素
makeheap()建立一个新的堆H
extractmin(H)返回优先队列H的最小值,同时将这个最小值从优先队列中删除。
decreasekey(H,x,k)把H中的某个值域为x的元素的值改成k
union(H1,H2):把H1和H2中的所有元素提取出来形成一个新的优先队列。
优先队列的基础数据结构可以是链表、二叉堆、二项式堆、斐波拉契堆,
基于不同的基础数据结构,优先队列的各个操作的时间复杂度的关系是:

不同优先队列的时间复杂度

可以看到基于链表的优先队列的性能最差,二叉堆、二项式堆、斐波拉契堆的性能依次优化。

基于链表的优先队列
我们可以使用链表或者数组这种数据结构来实现优先队列,我们假设选择的是数组。

假设数组里面的元素是有序的。
这种情况下,makeheap就是只要申请一个数组即可,时间复杂度为O(1)
insert 需要将元素插入合适的位置,选择合适的位置使用二分查询需要O(logn) O(logn)O(logn)时间,但是合适位置只有的元素都要向后移,时间复杂度为O(logn+n)=O(n) O(logn + n)=O(n)O(logn+n)=O(n)
extractmin取最小元素需要O(1),删除这个最小元素需要把后面的元素往前移动一个位置。
decreasekey修改某个元素的值。O(1)
delete删除需要O(n)
union也是需要O(n)
findmin只需要O(1)

假设数组里面的元素是无序的。
这种情况下,makeheap就是只要申请一个数组即可,时间复杂度为O(1)
insert 需要将元素插入到末尾,O(1)
extractmin取最小元素需要遍历整个数组,并将这个元素删除,需要O(n)
decreasekey修改某个元素的值。O(1)
delete删除需要O(n)
union也是需要O(n)
findmin只需要O(n)

因此使用数组的话,无论是否对数组进行排序,所需要性能都不是很好。

这是因为,我们让这个数组全部元素都有序实在太严格,甚至是有些浪费了。我们只是想知道最小的元素,但是是在是没有必要让整个数组有序。如果有某种方式,使得我们不用所有元素都有序也得得到当前最小值,那就好啦。

而这个就是二叉堆的核心思想:我们用树来存储所有的元素,然后我们让树的根比它的子节点的值都小就好啦。

基于二叉堆的优先队列

INSERT(x)函数
对于一个新的元素x,我们要把这个x插入到堆里面。那么我们的做法只需要
1.把x插到堆的末尾。注意一定要是末尾,如果不是末尾,这个堆对应的树就不是满二叉树了。
2.从刚刚插入的这个节点开始,从叶子节点往根节点更新。如果这个插入的值比父节点还小,那么这个刚刚插入的值就是以其父节点为根的子树的最小值,此时需要把这个节点的值与父节点的值交换。一直往上找,直到父节点比这个新插入的元素小位置或者到达根节点。
时间复杂度:最坏的情况下,这个新元素就是所有元素中的最小值。此时,从叶子到根遍历了logn个节点。
所以复杂度是O(logn)

FINDMIN()返回最小值
堆的根节点的元素就是当前最小值。

EXRACTMIN()返回最小值,同时将最小值删除
得到最小值就只需要当前的堆顶,先把这个堆顶的值保存。
然后将堆顶删除掉。
删除的方法是:
把堆的最后一个元素的值放到堆顶,删除最后一个元素。然后从堆顶开始,从上往下的维护堆。
时间复杂度:O(logn)

DECREASEKEY(ptr,value)把指针ptr指向的节点的值改成value.
这个问题分两种情况来看。
1.把ptr对应的节点的数据改小。
这种情况下,ptr对应的节点依然会小于其子节点。所以无需向下维护。
但是ptr节点的新值有可能小于父节点的值,所以需要从ptr向上的维护堆序。
例如:

2.把ptr对应的节点的数据改大。
这种情况下,父节点依然小于这个节点,于是该节点向上的部分可以不用去考虑。
但是该节点被改大以后,就不一定比其子节点都小。此时需要从这个节点开始递归的向下维护。单次维护的方式很简单,就是与其最小的那个子节点交换。然后交换后,从刚刚交换的子节点递归的向下维护。
例如:

UNION()将两个二叉堆合并
实际中是由将两个优先队列的元素合并在一起的需求的。
在二叉堆中,如果要将两个堆合并,有两种方法:

1.将两个堆的元素全部放在一块,然后对这些元素调用MAKEHEAP_LIST方法,在O(n)的时间内合并。

2.遍历其中一个堆的每个元素,将调用INSERT函数把这些元素插入到另外一个堆里面,需要O(nlogn)的时间。

**可以看到:二叉堆对于合并操作的支持为O(n),这个是很慢的!所以,人们提出了二项式堆来加速这个合并操作。

猜你喜欢

转载自blog.csdn.net/weixin_42130471/article/details/85415670