系列文章目录
文章目录
前言
一、堆的概念
- 如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。
- 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
二、堆的性质
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是(2^k) -1 ,则它就是满二叉树。
- 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。通俗来说k-1的深度时树都是满的,最后一层k的深度可以不满,但从左到右是连续的。
三、堆的实现
1.用数组定义堆结构struct Heap
代码如下:
struct Heap
{
HPDataType* a;
int size;
int capacity;
};
2.交换两个数Swap函数
代码如下:
void Swap(int* left, int* right)
{
int tmp = 0;
tmp = *left;
*left = *right;
*right = tmp;
}
3.向下调整算法AdjustDown函数
现在我们给出一个数组,逻辑上看做一颗完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
上图可知,先取出左右孩子小的节点,再和父节点比较,若比父节点小则交换两个节点,在把原来的孩子节点当做父节点一直往下遍历。
代码如下:
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = 2 * parent + 1;
while (child<n)//(n > 0)
{
if (child + 1 < n&&a[child + 1] < a[child])
{
child++;
}
if (a[child] < a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = 2 * parent + 1;
}
else
{
break;
}
}
}
4.向上调整算法AdjustUp函数
从倒数第一个非叶子节点从后往前,依次作为子树向下调整。
代码如下:
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
while ( child>0 ) //孩子大于0,则继续循环,否则终止循环
{
if (a[child]>a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else //已成为堆
{
break;
}
}
}
5.初始化堆HeapInit函数
因为堆物理结构是一个数组,所以要考虑插入数据是否要动态增容,所以先判断size和capacity的大小来决定是否要增容,然后建堆,建堆是建大堆,因为建小堆会打乱堆得结构,重新向下调整的话时间复杂度会增加。
代码如下:
void HeapInit(struct Heap* hp, HPDataType* a, int n) //初始化堆
{
assert(hp);
hp->a = (HPDataType*)malloc(sizeof(HPDataType)*n);
if (hp->a == NULL)
{
printf("malloc fail!\n");
exit(-1);
}
memcpy(hp->a, a, sizeof(HPDataType)*n);
hp->size = n;
hp->capacity=n;
//建大堆
int i = 0;
for (i = (hp->size - 2) / 2; i >= 0; i--)
{
AdjustDown(hp->a, hp->size, i);
}
}
6.销毁堆HeapDestory函数
直接释放掉malloc开辟的空间。
代码如下:
void HeapDestory(struct Heap* hp) //销毁堆
{
assert(hp);
free(hp->a);
hp->a = NULL;
hp->capacity = hp->size = 0;
}
7.堆的插入HeapPush函数
先判断是否要增容,如果不用则在堆得尾部插入就是size的下标,然后重新向上调整即可。
代码如下:
void HeapPush(struct Heap* hp, HPDataType x)
{
assert(hp);
if (hp->size == hp->capacity) //先判断容量是否充足
{
//用realloc在已有空间在开辟一块空间
HPDataType* tmp = (HPDataType*)realloc(hp->a, sizeof(HPDataType)* hp->capacity*2);
if (tmp == NULL)
{
printf("realloc fail!\n");
exit(-1);
}
hp->a = tmp;
hp->capacity = hp->capacity * 2;
}
hp->a[hp->size ] = x;
hp->size++;
AdjustUp(hp->a, hp->size - 1);
}
8.堆的删除HeapPop函数
采用堆顶数据和堆底数据交换,然后删除堆底数据,在重新向下调整。
代码如下:
void HeapPop(struct Heap* hp)
{
assert(hp);
assert(hp->size > 0);
Swap(&hp->a[0], &hp->a[hp->size - 1]);
hp->size--; //删除掉堆底数据
AdjustDown(hp->a, hp->size, 0);
}
9.取堆顶数据 HeapTop函数
因为物理结构是一个数组,所以取下标为0的数就是堆顶数据。
代码如下:
HPDataType HeapTop(struct Heap* hp)
{
assert(hp);
assert(hp->size != 0);
return hp->a[0];
}
10.取堆数据个数 HeapSize函数
直接输出size的大小。
代码如下:
HPDataType HeapSize(struct Heap* hp)
{
assert(hp);
return hp->size;
}
11.堆判空 HeapEmpty函数
直接判断堆数据size的大小是否为0。
代码如下:
bool HeapEmpty(struct Heap* hp)
{
assert(hp);
return hp->size == 0;
}
12.打印堆HeapPrint函数
直接遍历把堆中size个数的数据输出来。
代码如下:
void HeapPrint(struct Heap* hp) //打印堆
{
assert(hp);
int i = 0;
for (i = 0; i < hp->size; i++)
{
printf("%d ", hp->a[i]);
}
printf("\n");
}
总结
以上就是今天要讲的内容,本文仅仅简单介绍了堆的使用,对于堆是一个完全二叉树,而堆提供了大量能使我们快速便捷地处理数据的函数和方法,堆作为非线性表的一种,结构相对复杂,但是代码实现相对容易,所以我们要好好掌握堆结构。另外,如果有需要源码的私信我即可。还有,如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。