堆⚓︎
如何手写堆?
堆的介绍⚓︎
堆是一个完全二叉树:除了最后一层节点外,上面所有层节点都是满的,最后一层节点从左到右依次排布。
堆的类型:
- 小根堆(这里讨论):每个点的值都小于等于左右儿子的值,则根节点是最小值所在的点;
- 大根堆:父节点的值大于等于其子节点的值。
堆的存储方式:用 一维数组 存储。1
号点是根节点,采用左右孩子编号法,每个下标为x
的节点的左儿子(若存在)下标为2 * x
,右儿子(若存在)下标为2 * x + 1
;下标为i
节点的父节点(若存在)下标是i / 2
。
堆的基本操作⚓︎
基本函数:
down(x)
:把一个节点 往下调整;up(x)
:把一个节点 往上调整。
小根堆的操作(下标从1
开始):
- 插入一个数:在整个堆的最后一个位置插入
x
,也就是需要先把新元素从堆尾插入,再逐层上浮到合适位置 (heap[++size] = x; up(size);
); - 求集合当中的最小值:对于小根堆就是取出根节点
heap[1]
的值; - 删除最小值:(删除堆的顶点)因为这里删除位于尾部的节点较为简单,所以可以用整个堆的最后一个元素覆盖掉堆顶元素,也就是先把尾元素移到根上,再逐层下沉到合适位置 (
heap[1] = heap[size]; size--; down(1);
); - (STL的堆无内置实现) 删除任意一个元素:删除第
k
个点(heap[k] = heap[size]; size--; down(1); down(k); up(k);
),注意这里down
和up
只会执行一个; - (STL的堆无内置实现) 修改任意一个元素:修改第
k
个点(heap[k] = x; down(k); up(k);
)。
模板与例题⚓︎
堆的插入⚓︎
一次操作时间复杂度为 \(O(\log n)\)。
模板:
堆的删除⚓︎
一次操作时间复杂度为 \(O(\log n)\)。
模板:
例题:堆排序
堆排序和快速排序都是不稳定的排序,而归并排序是稳定的。
综合⚓︎
堆的插入与删除的STL代码⚓︎
时间复杂度为 \(O(n \log n)\)。
模拟堆⚓︎
例题:模拟堆
堆中的每次插入都是在堆尾,但是堆中经常有up
和down
操作,所以节点与节点的关系并不是用类似Trie的ne[idx][2]
就可以很好地维护的。但是好在堆是个完全二叉树,子父节点的关系可以通过下标来联系(左儿子2*n
,右儿子2*n + 1
)。就数组模拟来说,知道数组的下标就知道结点在堆中的位置。所以核心就在于即使有down
和up
操作也能维护堆数组的下标k
和节点idx
的映射关系。比如:h[k] = x
,h
数组存的是节点的值,按理来说应该用h[idx]
来存,但是节点位置总是在变的,因此可以维护k
和idx
的映射关系,比如说用ph
数组来表示ph[idx] = k
,那么结点值为h[ph[idx]]
,儿子下标为ph[idx] * 2
和ph[idx] * 2 + 1
,这样值和儿子节点可以通过idx
联系在一起了。
可见ph
数组主要用于帮助从idx
映射到下标k
,似乎有了ph
数组就可以完成所有操作了,但为什么还要有一个hp
数组呢?原因就在于在swap
操作中我们输入是堆数组的下标,无法知道每个堆数组的k
下标对应的idx
(第idx
个插入),所以需要hp
数组方便查找idx
。