堆
1. 概念
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树;
这里结点之间:父亲等于孩子 或小于孩子 或大于孩子;
若节点相等,则小根堆和大根堆都满足;
2. 分析堆进行插入数据的情况
如果该数组已满,此时再插入数据需要扩容,并且判断插入的数据是否满足堆的性质;
如果插入后不满足堆的性质,那我们需要向上调整;
如图所示,若孩子比双亲结点大,那么我们需要交换位置,直到满足堆的性质;
此时我们最多要交换的次数为堆的高度:log2(N+1) ~ log2(N) + 1
3. 堆的删除
堆的删除实际上是删除根节点,直接删除尾的结点很简单,但是删除后的结构还是一个堆,没有意义,因此堆的pop函数应该是删除根节点的元素,剩下的元素重新排列满足堆的结构;
堆删除的实际意义是:取出当前的最大值 / 最小值(无论是最大值还是最小值肯定是位于堆顶元素的位置上);
问题:怎么进行堆顶结点的删除?
分析:如果我们采用覆盖的方式进行删除,此时后面的元素全部往前移动,这时候的效率太低了,时间复杂度太高;除此之外,结点之间的关系也发生错乱了,父子兄弟之间的关系全部错乱了;
解决:将堆顶元素和最后一个元素的位置进行调换,然后再将最后一个元素进行删除(size--即可),最后需要进行向下调整,此时可以满足:根节点下的左子树和右子树之间的关系是满足堆的性质的(如下图所示);
4. 堆的排序
问题:实际应用中,给定我们的通常是一个数组,而不是直接点堆,那么我们怎么进行堆排序呢?
分析:直接将数组进行push组建成一个堆也可以,但是这样子做的时间复杂度太大了!
解决:
例如,我们由上面的数组,当我们插入第一个数据的时候,我们可以将后面的结点想象为孩子结点并进行向上调整,从而模拟建堆的过程(这里使用的是向上调整算法);
数据全部都插入,然后从第二个结点开始向上调整;
这种建堆的过程的时候复杂度为O(N * logn);
排升序是建立大堆还是小堆?
结论:
- 排升序需要建立大堆;
- 排降序需要建立小堆;
分析:如果我们排升序建立小堆
每次pop之后结点的位置直接就消失,此时左右孩子结点会成为新的父亲节点,导致下面的顺序就乱了!
因此我们每次选出一个最小值,都需要建堆,而建堆的时间复杂度是O(logN * N)!
效率太地下!
相反如果排升序建大堆:
此时根节点是最大值,然后我们将根节点和最后一位的叶子结点进行交换,最大值就位于叶子节点,然后我们进行size--,分析0~size-2位置上的其他节点即可,且中间的堆的结构没有被破坏!
向上调整和向下调整的前提关键:
左右子树必须是大堆或者小堆!
堆排序的代码如下所示(前提是已经有了向下调整的代码):
5. 建堆的过程
堆的创建有两种方法:通过向上调整 / 通过向下调整
向下调整
对于向下调整函数来说,有3个参数:指向的数组,数组的元素个数,父亲结点i;
因此分析上面的图:向下调整函数实际上就是先调整 3 所在的父结点,即 6,然后再调整7、5、1、2,即根据父亲节点来调整!
使用向下调整来建堆的过程的时间复杂度为O(N) = N;
向上调整
对于向上调整来说,整体的时间复杂度是O(N) = log(N) * N;
因此向下调整的效率比向上调整的效率高很多!
其实可以看出:
对于向下调整来说:调整的是第一层到倒数第二层!(将不满足的数据移动到下面,最后一层不需要调整);
对于向上调整来说:调整的是第二层到最后一层!(将不满足的数据移动到上面,第一层不需要调整);
但是此时有一个致命的因素:最后一层的数据最多的情况下是上面所有层数的数据之和!因此向下调整需要调节的数据更少,效率更高!
根据堆进行排序
当我们有了堆的结构之后,此时根节点的值就是我们需要的当前的最大值,那么排序的思路就是:将最大值换到最后一个节点的位置上,然后再将除了最后一个节点的剩下部分进行向下调整,使其结构再次成为一个堆,再重复上面的操作!
这里的n指的是数组元素的个数,end指的是最后一个元素的下标;
这里排序的时间复杂度是O(N) = N * logN;
堆的应用
堆的应用主要包括:排序和topK问题;
关于堆排序的实际应用我们后面讲排序的时候再讲解。
堆的模拟实现代码
heap.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
#include<time.h>
typedef int HPDateType;
typedef struct Heap
{
HPDateType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php);
void HeapPush(HP* php ,HPDateType x);
// 向上调整 child是需要调整的坐标
void AdjustUp(HPDateType* a, int child);
void Swap(HPDateType* x, HPDateType* y);
void HeapPop (HP* php);
// 向下调整
// n为数据的个数
void AdjustDown(HPDateType* a, int n, int parent);
size_t HeapSize(HP* php);
bool HeapEmpty(HP* php);
HPDateType HeapTop(HP* php);
void HeapDestory(HP* php);
heap.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"heap.h"
void HeapInit(HP* php)
{
assert(php);
// 默认开辟四个空间
php->a = (HPDateType*)malloc(sizeof(HPDateType) * 4);
if (php->a == NULL)
{
perror("malloc fail...");
return;
}
php->size = 0;
php->capacity = 4;
}
void Swap(HPDateType* x, HPDateType* y)
{
HPDateType tmp = *x;
*x = *y;
*y = tmp;
}
// 向上调整
void AdjustUp(HPDateType* a, int child)
{
int parent = (child - 1) / 2;
// 比较孩子和父结点
while (child > 0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child-1) / 2;
}
else
{
break;
}
}
}
// 尾处插入数据,然后调整
void HeapPush(HP* php, HPDateType x)
{
assert(php);
if (php->size == php->capacity)
{
// 扩容
HPDateType* tmp = (HPDateType*)realloc(php->a, sizeof(HPDateType) * php->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
// 尾巴处插入数据
php->a[php->size] = x;
php->size++;
// 向上调整
AdjustUp(php->a, php->size -1);
}
// 向下调整
void AdjustDown(HPDateType* a, int sz, int parent)
{
// 假设当前左孩子为孩子(下标)
int child = 2 * parent + 1;
while (child < sz)
{
if (child + 1 < sz && a[child] < a[child + 1])
{
// 右孩子为当前的孩子
child++;
}
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
parent = child;
child = parent + 1;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
// 头和尾交换
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
// 向下调整
AdjustDown(php->a, php->size, 0);
}
HPDateType HeapTop(HP* hp)
{
return hp->a[0];
}
size_t HeapSize(HP* hp)
{
assert(hp);
return hp->size;
}
bool HeapEmpty(HP* hp)
{
assert(hp);
return hp->size == 0;
}
void HeapDestory(HP* hp)
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->size = 0;
hp->capacity = 0;
}