堆:
采用完全二叉树进行存储,有两个特性:
例如,下图左边两个完全二叉树是最大堆,右边两个是最小堆。
但要注意堆一定要是完全二叉树,比如下图左边两个因为不符合完全二叉树,所以不是堆。下图左数第三个看似是最小堆,但是因为节点16比其子树15大,所以不符合,也不是堆。
所以堆的特点是:
从根节点到任意节点路径上节点序列的有序性
最大堆的创建:
注意这里创建的最大堆,堆的第一个元素设置了一个哨兵,也就是设置了一个大于堆中所有可能的元素的值,以便于后续操作。
最大堆的插入:
以下图为例,要在图中第[6]个位置插入节点58:
此时如果把58插在[6]处,因为比它的父节点31大,所以破坏了最大堆的规则。此时将58和31换位置。结果如下
此时还是不行,因为根节点[1],也就是44,应该是所有树里最大的值,所以58和44再换一次位置,得到结果如下:
这里注意到,每次调换,如果要换第[i]个节点,都是将该节点和其父节点,也就是第[i/2](i/2向下取整)个节点调换。上图中,第[6]个节点一开始和[3]调换,后来和[1]调换。
实现插入操作的伪代码如下:
这里要注意一种情况,如下图,如果要插入的节点是25,比原本的根节点20都要大,这时由于循环判断的条件是一直和父节点比大小判断,此时就会导致溢出。
但是这里由于设置了哨兵,所以当比较到第[0]个元素时,25小于1000,所以此时25和第[1]个节点20调换。就解决了问题。
或者为了避免这种情况,如果不设置哨兵,就要在下图位置加一个循环终止条件:i>1
最大堆的删除
1.要删除根节点(即最大值):
把按顺序的最后一个点替换根节点,再去跟其左右子树比较大小,移动。
比如要删除下面这个最大堆的根节点58
58删除后用最后一个节点,也就是第[5]个节点31,补到第[1]个节点。结果如下。由于是最大堆,此时31作为根节点,找出其较大的儿子,即其左儿子,为44,比31大,就将44和31互换。
44和31互换后,此时31的左儿子为35,仍大于31,所以将31和35进行调换,最终结果如下:
可以看出其时间复杂度为T(N)=O(log N),即为树的高度。
最大堆删除操作伪代码:
先判别堆是否为空。
接下来把要删掉的元素保存在MaxItem中,程序最终要将该MaxItem值return出去。以下图中的最大堆为例就是Elements[1],即58.
接下来的操作是为了把最后一个元素,也就是Elements[size]换上来取代根节点。这里首先把最后一个元素存在temp中,这里因为删掉了一个元素,所以 Size-- 代码如下
接下来是一个循环,该循环用于找插入点,即找到这个temp该放在哪里。这里从根节点位置从上往下找。所以设根节点的位置Parent为1开始找。这个Parent就指示着最终该节点要插入的位置。下图中最后一行,找到了插入位置,进行插入。
测试习题1:
最大堆的建立:
有两种方法,如上图所示。第一种方法的时间复杂度最大为O(NlogN)是因为每次插入一个节点,找到他要放的位置,其时间复杂度是log 2 N(即以2为底),插入N个节点即该操作执行N次,就是Nlog 2 N
比较可知明显方法二更便捷。对于方法2举例如下:
对该树采用方法二建堆。建堆的方法与上面最大堆删除堆顶元素的方法非常相似。
在删除操作中,一般是把堆顶元素拿掉后,将堆中的最后一个元素放到堆顶(下图红色),此时堆顶的左子树和右子树都是两个排列好的堆(下图蓝色)。此时的任务是怎么把这个红色堆顶节点进行调整使这整个变成一个堆。调整的方法就是和他的左右儿子不断比较,最后挑一个合适的放上来。
回到这里的例子,可以看到堆顶元素79两侧都不构成堆,对于节点66,43来说也是一样的,下面都未构成堆。所以采用自底向上的方法构建。
首先选取倒数第一个有儿子的节点开始,即图中的87,选取这个节点的原因是其左右都最多只有一个儿子结点,而一个单个节点就可以看作一个堆。从这里开始一层一层向上转换。
87和其唯一的子树9比较,87>9,不改变位置。此时以87为根节点的小的堆调整完毕。后退一个,对以30为根节点的堆进行调整。如下图,这里将30和72互换。
再向前,调整83节点的堆。
再倒回去一个节点,调整以43为顶的堆(此时43的左右两侧都已成为两个堆)。
再向前,调整66。66先与两个儿子的较大者91比较,66<91,互换。再与两个儿子较大者比较,66<83再次互换。
最后再向前一个,调整到堆顶。
到这里堆建立完成。
例题:
比如要把一个如上图结构的最大堆改成最小堆,就会出现这种最坏情况,如下图:
练习题如下: