【二叉堆定义与存储结构】
二叉堆是具备以下性质的完全二叉树:
- 小根堆:每个结点的值都小于等于其左右孩子结点的值,堆顶元素是堆中最大值
- 大根堆:每个结点的值都大于等于其左右孩子结点的值,堆顶元素是堆中最小值
若将堆按照层序顺序从根结点开始编号,根结点序号为 $1$,用 $heap[i]$ 表示第 $i$ 个结点,则 $heap[i]$ 的两个儿子为:$heap[2i]$ 与 $heap[2i+1]$
且在小根堆中,满足:
在大根堆中,满足:
【插入操作与向上调整】
插入操作是指向二叉堆中插入一个元素,且要保证插入后二叉堆的结构不会改变
最简单的方式就是在最下一层最右边的叶结点后插入新的元素,若最后一层已满,就新增一层
以大根堆为例,在插入新结点后,如果这个结点的权值大于其父结点的权值,就进行向上调整操作,将其与其父结点进行交换,然后重复该过程,直到不满足或到根为止
同理,当堆为小根堆时,若这个结点的权值小于其父结点的权值,同样进行向上调整操作
向上调整操作的时间复杂度为 $O(\log n)$
大根堆的插入操作的实现如下:
1 | int heap[N]; //heap[1]为堆顶 |
【删除操作与向下调整】
插入操作是指删除二叉堆的堆顶元素,且要保证删除后二叉堆的结构不会改变
如果直接进行删除,那么堆会变为两个,难以处理
因此,可以将删除操作等价为插入操作的逆过程,即设法将根结点移动到二叉堆中的最后一个结点,然后再进行删除
通常的做法是,先将根结点与最后一个结点交换,然后删除位于最后一个结点处的根结点
但这样新得到的堆显然不满足堆的性质,为此,要进行向下调整
同样以大根堆为例,在得到新的堆后,在新堆的根结点的儿子中,找一个值最大的结点,然后将他们交换,并重复该过程,直到底层为止
同理,当堆为小根堆时,找一个值最小的儿子结点,进行向下调整操作即可
向下调整操作的时间复杂度为 $O(\log n)$
1 | int heap[N]; //heap[1]为堆顶 |
【堆的建立】
对于一个空的二叉堆,当从头开始建立时,若不考虑元素顺序,只需要一个个的将 $n$ 个元素进行插入操作,之后,从根开始,按 BFS 序进行向上调整,使得建立的二叉堆满足堆的性质
1 | int heap[N]; //heap[1]为堆顶 |
这样一来,对于第 $k$ 层的结点,向上调整的复杂度为 $O(k)$,而不是 $O(\log n)$
但以总时间复杂度来说,有:
为此,可以换一种思路,即在对 $n$ 个元素插入后,从叶结点开始进行向下调整
1 | int heap[N]; //heap[1]为堆顶 |
可以发现,叶结点无需调整,且结点向下调整的时间复杂度为 $O(\log n-k)$
因此,可以从序列约 $\frac{n}{2}$ 的位置处开始调整,从而在不影响复杂度的情况下减少参数,以总时间复杂度来说,有:
之所以能够以 $O(n)$ 的方式建堆,是因为堆的形式很弱,二叉堆并不是唯一的