堆是是一个按照完全二叉树存储方式存储的一维数组.但具有了一些二叉树没有的性质.
堆分大堆和小堆.
大堆:
堆的根节点最大,并对于其中的元素{k1 ,k2 ,k3 ,…, },满足:
既任意一个结点得值永远大于等于它的孩子结点的值.
小堆
堆的根节点最小,并对于其中的元素{k1 ,k2 ,k3 ,…, },满足:
既任意一个结点的值永远小于等于它的孩子结点的值
我们用图来观察一下它的逻辑结构和在内存中如何存储的存储结构.
可以看到,逻辑结构确实一个完全二叉树,存储结构是一个一维数组.
上面那张图能看明白就可以了.
先来看堆的结构,是一个一维数组,然后可以有当然大小和总容量,结构体如下:
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}H P;
下面来实现堆的一些基本操作.
目录
堆的初始化
这个说了好多遍了,将数组置为空,其他的置为0.
void HeaoInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
堆的销毁
都是常规操作,前面文章都有说
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
输出堆中的元素
这个也很简单,就是将数组中的元素全部输出出来,写一个循环就可以了.
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
堆插入
我们插入一定是从堆的尾部插入.但是如果插入之后,会导致一些问题,比如正常的大根堆:
这个时候都是父节点一直大于它的孩子结点.
这个时候我们插入一个40,如下:
这个时候我们发现30的孩子结点 10 和 40,此时已经不满足父节点一直大于等于孩子结点,因为30 < 40,所以我们这个时候需要调整这个堆,使它重新满足这个条件.
这个时候我们需要用到向上调整算法,既从下向上开始调整直到满足 父节点的值一直大于等于孩子结点的值 (大堆) 或者 父节点的值一直小于等于孩子结点的值(小堆).
可以直接向下拉,看这个具体的详细过程.
调整完之后,堆的插入也就完成了.
void HeapPush(HP* php, HPDataType x)
{
assert(php);
//如果此数组里元素的数量等于容量,则需要扩容
//如果不理解可以看前面几节的文章,有详细的说明
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("Realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
//插入
php->a[php->size] = x;
php->size++;
//插入完成后需要整体调整一下堆(向上调整算法)
AdjustUp(php->a, php->size - 1);
}
堆删除
(删除指的是删除堆顶的元素)
思路:
若直接删除堆顶元素,则堆的结构被破坏,可以采用将第二个元素到最后一个元素每个元素都使用一次向上调整算法,但是效率太低.
所以我们的方法是这样的:
先将堆中的第一个元素和最后一个元素进行交换,然后再将最后一个元素删除,这样虽然是一个堆的结构,但是堆的结构被破坏了,我们可以采用向下调整算法,从堆顶向下重新调整结构,使之符合大顶堆或小顶堆.
向下调整算法下划可以看到.
代码如下:
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
☆堆的向上调整算法
向上调整堆的办法呢,就是先找出父节点的下标,公式:parent =(child-1)/ 2.其中,parent为父结点下标,child为子结点下标.
然后循环不断让父节点和子节点比较,这里我们以大堆为例,若父节点小于孩子结点,则将两个值交换,并将此时父亲结点更新为孩子结点,以继续和父亲的父亲的结点比较.知道父节点的下标小于0,或者此时的父节点已经大于孩子结点.
代码如下:
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)//注意这里是child>0,而不是parent>=0.因为parent=0时,进入循环后,child=parent=0,然后parent=(0-1)/2还是等于0,所以这样写错误。只有当child=0时或已经满足条件时,才是真正的循环结束.
{
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
☆堆的向下调整算法
向下调整算法的思路是:先求出根据父结点求出它的左子结点,
公式为 child = parent * 2 + 1.
再+1则是右子结点,两个子结点先相互比较选出的大的那个子结点,再让这个大的子结点和父结点进行比较,这里以大堆为例,
若子结点大于父结点,则交换父子结点,同时将父结点更新为原来子结点的下标,如此可以迭代向下进行.
若子结点小于父结点,此时已经满足条件,break跳出循环即可.
代码如下:
void AdjustDown(HPDataType* a, int size, int parent)
{
//先求子结点
int child = parent * 2 + 1;
while (child < size)
{
//左右子结点比较,若左子结点小于右子结点,则更新child为右子结点
if (child + 1 < size && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
取得堆顶元素
判断堆中是否有元素,若有,则直接返回第一个元素.
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
堆的判空
还是老套路,判断size是否等于0就好了.
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
堆的大小
返回size.
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
上面就是关于堆的一些基本操作了,其中最需要掌握的是向上和向下调整算法.
下一节讲解堆排序和TopK问题,也是非常经典和重要的,上面两个算法一定要学会!
如果有遗漏或错误之处,欢迎补充或指正~
总代码:
Heap.h文件:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
void HeapInit(HP* php);
void HeapDestroy(HP* php);
void HeapPrint(HP* php);
void AdjustUp(HPDataType* a, int child);
void HeapPush(HP* php, HPDataType x);
void AdjustDown(HPDataType* a, int size, int parent);
void HeapPop(HP* php);
HPDataType HeapTop(HP* php);
bool HeapEmpty(HP* php);
int HeapSize(HP* php);
Heap.cpp文件
#include"Heap.h"
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while (child > 0)
{
if (a[parent] < a[child])
{
Swap(&a[parent], &a[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapPush(HP* php, HPDataType x)
{
assert(php);
//如果此数组里元素的数量等于容量,则需要扩容
//如果不理解可以看前面几节的文章,有详细的说明
if (php->size == php->capacity)
{
int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("Realloc fail\n");
exit(-1);
}
php->a = tmp;
php->capacity = newcapacity;
}
//插入
php->a[php->size] = x;
php->size++;
//插入完成后需要整体调整一下堆(向上调整算法)
AdjustUp(php->a, php->size - 1);
}
void AdjustDown(HPDataType* a, int size, int parent)
{
//先求子结点
int child = parent * 2 + 1;
while (child < size)
{
//左右子结点比较,若左子结点小于右子结点,则更新child为右子结点
if (child + 1 < size && a[child] < a[child + 1])
{
child++;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a, php->size, 0);
}
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
Test.cpp文件:
这个文件自行测试各模块功能哦.
#include"Heap.h"
int main()
{
HP h1;
HeapInit(&h1);
HeapPush(&h1, 1);
HeapPush(&h1, 2);
HeapPush(&h1, 3);
HeapPush(&h1, 4);
HeapPush(&h1, 5);
HeapPop(&h1);
HeapPrint(&h1);
return 0;
}