文章目录
- 前言
- 一、堆的概念和结构
- 二、堆的调整算法
- 向下调整算法
- 向上调整算法
- 两种算法建堆的时间复杂度
- 三、堆的实现
- 结构体定义
- 初始化和销毁
- 堆的插入
- 堆的删除
- 挪移数据覆盖删除
- 首尾交换再删除
- 获取堆顶元素
- 获取有效数据个数
- 判断是否为空
- 总结
前言
继续,本篇较难
正文开始!
一、堆的概念和结构
如果有一个关键码的集合K ={k0,k1,k2,…,kn-1},把它的所有元素按照完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1且 Ki <= K2i+2 (Ki >= K2i+1且 Ki >= K2i+2) i =0, 1, 2…则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆
额,这很严谨,有点高数概念的味道,我换种说法:
堆就是以完全二叉树的顺序存储方式来存储元素,同时又要满足父亲结点存储数据都要大于等于儿子结点存储数据(也可以是父亲结点数据都要小于等于儿子结点数据)的一种数据结构。堆只有两种即大堆和小堆,大堆就是父亲结点数据大于等于儿子结点数据,小堆则反之
所以,其实我们可以得出堆的两点性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值
- 堆总是一棵完全二叉树
二、堆的调整算法
以下代码部分是根据建小堆来走,如果需要建大堆可以修改直接的大于小于号
向下调整算法
现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆
但是要注意,这是有前提的!:
- 若想将其调整为小堆,那么根结点的左右子树必须都为小堆
- 若想将其调整为大堆,那么根结点的左右子树必须都为大堆
向下调整算法的基本思想(小堆):
- 从根结点处开始,选出左右孩子中值较小的孩子
- 让小的孩子与其父亲进行比较:若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止,若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustDown(HPDataType* a, int size, int parent)
{
//1.假设左孩子为小的数据
int child = parent * 2 + 1;
while (child < size)
{
//2.如果左孩子>右孩子 则将右孩子赋值
//有可能只有左孩子 所以加条件
//以下未有左右孩子且左孩子>右孩子情况,则将child++
if (child + 1 < size && a[child] > a[child + 1])
{
child++;
}
//3.将孩子与父亲进行比较 如果孩子小则交换
//然后将父亲和孩子移动到下一个位置
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
向上调整算法
当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法
向上调整算法的基本思想(小堆):
- 将目标结点与其父结点比较
- 若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了
同样的,向上调整算法也有一个前提:
- 若想将其调整为小堆,那么原来的数据为小堆。
- 若想将其调整为大堆,那么原来的数据为大堆。
void Swap(HPDataType* a, HPDataType* b)
{
HPDataType tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0) // 请思考一下是child > 0
{
if (a[parent] > a[child])
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
两种算法建堆的时间复杂度
一、向下调整算法
我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可
所以向下调整算法的时间复杂度为O(N)
二、向上调整算法
这时候我们就需要从第二个节点到最后一个节点,依次向上调整建堆了
所以向上调整算法的时间复杂度为O(NlogN)
因此,对于任意的一维数组(逻辑上看作是树),我们同一采用向下调整算法,从倒数第一个非叶子节点开始,因为它的时间复杂度比较低!
三、堆的实现
在有了前文向上调整和向下调整这两个利器之后,我们就可以来实现堆了
结构体定义
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;//存放数据的动态数组
int size; //有效数据个数
int capacity; //数组容量
}HP;
初始化和销毁
void HeapInit(HP* php)
{
assert(php);//断言避免出现空指针
php->a = NULL;
php->capacity = php->size = 0;
}
void HeapDestory(HP* php)
{
assert(php);
free(php->a);//释放动态数组
php->size = php->capacity = 0;
free(php);
php = NULL;
}
堆的插入
思路是先将新数据插入到一维数组(逻辑上是堆)的末尾,因为未插入之前是堆,插入后要还是堆,我们之间向上调整即可
void HeapPush(HP* php, HPDataType x)
{
assert(php);
//1.检查容量
if (php->size == php->capacity)
{
int newCapacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType)*newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
php->a = tmp;
php->capacity = newCapacity;
}
//2.插入数据
php->a[php->size] = x;
php->size++;
//3.调整数据
AdjustUp(php->a, php->size-1);
}
堆的删除
关于堆的删除,我们一般默认规定删除堆顶也是就是根节点,至于删除尾部数据意义不大,因为尾部数据没有特别的地方,既不是最大,也不是最小
挪移数据覆盖删除
挪移数据覆盖会导致堆发生严重BUG,整棵树的父子关系全乱,也就是需要维持大小关系乱了
倒反天罡!
首尾交换再删除
对于堆的删除,我们采用另外一种方法,首尾交换再删除,左右子树依旧是堆,同时关系也没有乱,并且删除堆顶数据通过尾删再向下调整代价很低
思路也简单,就是先交换头尾元素,然后再size自减,最后头节点向下调整
void HeapPop(HP* php)
{
assert(php);
//有数据才删除
assert(php->size > 0);
//1.将首位数据交换
Swap(&php->a[0], &php->a[php->size - 1]);
//2.删除尾数据
php->size--;
//3.向下调整
AdjustDown(php->a, php->size, 0);
}
获取堆顶元素
这很简单了,直接返回根节点数据即可
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
获取有效数据个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
判断是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
总结
哈哈,初步感受到堆的巧妙了吧,我们后面继续来学习堆的实际应用!