堆
堆其实可以看成是完全二叉树,即除了最后一层其他层全满,最后一层的叶子结点靠左边的二叉树。
其次,堆的每个结点的值必须大于等于(或者小于等于)其子树的中每个结点的值,根据这个性质,我们可以知道堆的根结点的值是整个堆中最大(或者最小的),也称为大顶堆(小顶堆)。
堆的实现
存储
之前在二叉树那一篇中提到过,完全二叉树非常适合用数组存储(以上图大顶堆为例):
第一种:下标从1开始存储,在不超边界的情况下,假设当前结点的下标为i,左子树的下标为i*2,右子树的下标为i*2+1,父节点的下标为i/2。
值 | 90 | 70 | 80 | 60 | 10 | 40 | 50 | 30 | 20 | |
---|---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
第二种:下标从0开始存储,在不超边界的情况下,假设当前结点的下标为i,左子树的下标为i*2+1,右子树的下标为i*2+2,父节点的下标为(i-1)/2。
值 | 90 | 70 | 80 | 60 | 10 | 40 | 50 | 30 | 20 |
---|---|---|---|---|---|---|---|---|---|
下标 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
插入
往堆插入一个元素,为了继续满足完全二叉树的特点,所以我们将这个元素加在堆的末尾或者说数组的末尾。
其次,还需要满足大顶堆(小顶堆)的特点,本文以大顶堆为例。要保持大顶堆的特点,需要进行堆化,堆化又分为自上而下和自下而上。
自上而下的堆化:拿当前结点n和左右孩子比较,如果左右孩子得值有比n大的,那么把大的那个结点的值与n进行交换,从大的孩子结点继续往下进行上面的比较,直到叶子结点;如果左右孩子没有比n大的就直接结束。因为是一直和孩子结点进行比较,所以是从上往下的。
自下而上的堆化:拿当前结点n和父节点比较,如果比父节点大,那么就交换值,从父节点开始,直到根节点。如果没有比父节点大就直接结束。和上面差不多,不过不是和孩子结点比较,而是和父节点进行比较,所以是从下往上的。
这里我们将一个结点插入到末尾,所以是从下往上进行堆化,来看下面这个例子:
我们插入21,加到堆的末尾,然后开始自下而上的堆化:21>父节点2所以交换值,然后接着在父节点的位置比较21>根节点20所以交换值,到根节点了结束,可以看到堆化后继续保持了大顶堆的特点。
插入的代码如下:
void insert(T data) {
//size是已经装了的元素个数,capacity是数组申请的空间大小
if (size >= capacity)return; //堆满了,这里没有写扩容的逻辑
++size;
arr[size] = data;
int i = size;
while ((i - 1) / 2 > 0 && arr[(i - 1) / 2] < arr[i]) {
//(i - 1) / 2是父节点的下标,自下而上堆化
std::swap(arr[(i - 1) / 2], arr[i]);
i = (i - 1) / 2;
}
删除堆顶元素
删除堆顶元素我们需要用到自上而下的堆化,这里我们将最后一个元素与堆顶元素交换,然后再从根节点自上而下堆化:
void removeMax() {
if (size == 0)return;
arr[0] = arr[size]; //将最后一个元素换到根节点的位置
--size;
heapify(size, 1);//然后自上而下堆化
}
void heapify(int n, int i) {
//自上而下堆,n表示数组最末尾的下标,从下标i开始进行堆化
while (1) {
int maxPos = i;
if (i * 2 + 1 >= n && arr[i] < arr[i * 2 + 1])maxPos = i * 2 + 1;
if (i * 2 + 2 >= n && arr[maxPos] < arr[i * 2 + 2])maxPos = i * 2 + 2;
if (maxPos == i)break;
std::swap(arr[i], arr[maxPos]);
i = maxPos