前言
堆分为两种,一种是大堆(大根堆),还有一种是小堆(小根堆)。本篇文章将会通过实现大堆的方式来让大家理解堆方面的知识。小堆可根据大堆稍做修改即可。
文章末尾附带源码。
一、堆的概念及结构
堆是一种特殊的数据结构,本质上是一种完全二叉树。堆的特点是,如果是小堆,那么所有的父结点都要比其子结点要小,大堆则相反,所有的父结点都要比其子结点要大。由此可知,大堆的根结点肯定是整个堆的最大值,小堆的根结点肯定是整个堆的最小值。
堆在逻辑结构上是一颗完全二叉树。堆在物理结构上是一个数组。
其中很容易看出,每个父结点都是比其子结点要大,根结点也是堆中的最大值。
二、头文件的编写
我们创建一个头文件,叫做 “Heap.h” 。
1.引入库函数头文件
#include<stdio.h> #include<stdlib.h> #include<assert.h> #include<stdbool.h>
2.定义堆结构体
// 宏定义数据类型 typedef int HPDataType; // 堆结构体 typedef struct Heap { // 指向数组的指针,数组的元素类型为HPDataType HPDataType* a; // 数组、堆的有效元素个数 int size; // 数组的容量 int capacity; }HP;
在一般情况下,堆都是存放的都是 int 类型的数据,所以其实也可以不用 typedef 来宏定义数据类型。堆一般都是采用顺序表的结构来存储的,所以我们在创建堆结构体的时候,模仿顺序表的结构体就可以了。
3.声明功能函数
// 堆的初始化 void HeapInit(HP* php); // 销毁 void HeapDestroy(HP* php); // 往堆中插入数据 void HeapPush(HP* php, HPDataType x); // 删除堆顶元素 void HeapPop(HP* php); // 查询堆顶元素 HPDataType HeapTop(HP* php); // 查询堆的有效数据个数 int HeapSize(HP* php); // 判断堆是否为空,为空则真 bool HeapEmpty(HP* php);
三、 主函数文件的编写
我们创建一个源文件,叫做 “test.c” 。
1.包含头文件
#include"Heap.h"
2.编写测试用例
void Test01() { // 创建一个无序集合 int arr[] = { 245,7345,123,874,43,78,235,6457,1235,784,34,63,2 }; // 创建一个堆 HP hp; // 将堆初始化 HeapInit(&hp); // 将集合中的元素挨个插入到堆里面 for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) { HeapPush(&hp, arr[i]); } // 如果堆不为空 while (!HeapEmpty(&hp)) { // 打印堆顶元素 printf("%d ", HeapTop(&hp)); // 删除堆顶元素 HeapPop(&hp); } printf("\n"); // 销毁堆 HeapDestroy(&hp); }
3.主函数的编写
int main() { // 调用测试用例 Test01(); // 程序正常结束 return 0; }
四、功能函数的编写
我们创建一个源文件,叫做 “Heap.c” 。
1.包含头文件
#include"Heap.h"
2.堆的初始化
void HeapInit(HP* php) { // php为指向堆结构体的指针,不管堆里是否有元素,堆结构体都存在,所以该指针不可能为空,可以断言一下 assert(php); // 指向数组的指针初始化为不指向任何空间 php->a = NULL; // 堆的有效数据个数为0个 php->size = 0; // 数组的容量为0 php->capacity = 0; }
对于堆的初始化,我们将堆初始化为不包含任何元素。并且存储元素的空间也没有被开辟。我们采用动态分配空间的方式存储堆的元素,以避免空间的浪费,所以在初始化的时候,容量可以被赋值为0。
3.堆的销毁
void HeapDestroy(HP* php) { // php不可能为空,值得断言一下 assert(php); // 释放数组空间 free(php->a); // 释放空间后,指针需要置空 php->a = NULL; // 堆的有效数据变为0 php->size = 0; // 数组的容量变为0 php->capacity = 0; }
由于堆是由顺序表实现的,所有的数据都存放在一个数组里面,我们可以直接通过释放数组的空间来销毁所有的数据,同时将堆结构体的值重置为初始化状态即可。
4.往堆里面插入数据
void HeapPush(HP* php, HPDataType x) { // php不可能为空 assert(php); // 扩容情况,如果堆的有效数据个数与容量相同 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; } // 将待插入的值插入到堆的末尾 php->a[php->size] = x; // 堆的有效数据个数+1 ++php->size; // 数据插入到堆的末尾后需要将其调整位置,令新数据移动到其正确的位置 AdjustUp(php->a, php->size - 1); }
在向堆中插入数据时,第一件事就是需要判断堆是否已经满了,堆满了自然是不能插入,需要进行扩容操作。我们堆是使用顺序表实现的,顺序表非常擅长在尾部插入删除,所以我们插入数据的时候,直接插入到堆的末尾即可,但是,插入到末尾就结束了吗,那肯定不是,该数据有很大可能不应该在这个位置,而在堆的其他位置,所以我们需要调整这个元素的位置。于是我们创建了一个 AdjustUp 函数来实现调整。
5.将堆的末尾元素向上调整到正确位置
// 参数为指向数组的指针和待调整元素的下标 void AdjustUp(HPDataType* 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; } } }
在插入新数据后,由于新数据可能不应该在那个位置,所以需要进行调整。大堆的定义就是所有的父结点都比其子结点要大,所以插入的数据不断的跟其祖先进行比较,如果比父结点大,则跟父结点的值进行交换,如果比父结点小,说明新数据到了其应该到的位置,结束调整即可。
6.交换函数
// 由于交换的是两个堆的元素,所以参数是两个指向堆元素的指针 void Swap(HPDataType* p1, HPDataType* p2) { // 创建临时变量赋值为p1指向的空间的数据 HPDataType tmp = *p1; // 将p2指向的空间的数据赋值给p1指向的空间的数据 *p1 = *p2; // 将原p1指向的空间的数据赋值给p2指向的空间的数据 *p2 = tmp; }
交换函数不多说,要注意的是,形参只是实参的一份临时拷贝,改变形参的值是不能改变实参的值的,所以需要通过指针的方式来间接改变两个数据的值。
7.删除堆顶的元素
void HeapPop(HP* php) { // php不能为空 assert(php); // 堆必须有数据才能删除 assert(php->size > 0); // 交换堆顶元素和堆末尾的元素的数据 Swap(&php->a[0], &php->a[php->size - 1]); // 使堆的有效数据-1 --php->size; // 将原堆末尾的元素移到堆顶后需要进行调整位置,使其移动到其应该在的位置 AdjustDown(php->a, php->size); }
堆的删除就是删除堆顶元素,因为堆只有堆顶元素是最特殊的,在大堆中,堆顶元素就是堆的最大值,在小堆中,堆顶元素就是堆的最小值。我们删除的时候当然不能直接删除第一个元素,因为这样操作会打乱堆的结构。最有效的方法就是,将堆顶元素与堆的最后一个元素进行交换,然后直接令堆的有效数据 -1 即可。此时,堆顶元素变成了原来的堆的末尾的元素,位置是肯定不符合的,于是我们最后将其与其孩子进行比较,向下调整即可。
8.将堆顶元素向下调整到正确位置
// 向下调整函数需要的参数有指向数组的指针和堆的有效数据个数 void AdjustDown(HPDataType* a, int size) { // 待调整元素为堆顶元素,下标为0 int parent = 0; // 待调整元素的左孩子的下标 int child = parent * 2 + 1; // 待调整元素的左孩子的下标必须是堆的有效数据下标内,否则孩子不存在 while (child < size) { // 当右孩子存在并且右孩子比左孩子要大时 if (child + 1 < size && a[child] < a[child + 1])// 小堆则修改第二个 < 为 > { // 找出左右孩子中最大值 ++child; } // 如果最大的孩子要比父结点大,说明该孩子更应该做父结点,应该与待调整元素进行交换 if (a[parent] < a[child]) { // 交换待调整元素与孩子的值 Swap(&a[parent], &a[child]); // 原孩子下标成为新的待调整元素的下标 parent = child; // 找到新的左孩子下标 child = parent * 2 + 1; } // 如果最大的孩子没有父结点大,说明待调整元素以及到了其应该在的位置 else { // 跳出循环,结束调整 break; } } }
在将堆的末尾元素交换到堆顶后,堆的结构发生了改变,我们需要将堆顶元素进行调整,使其回到其应该在的位置,让堆的结构恢复。在待调整元素向下调整中,待调整元素需要不断与其最大的孩子进行比较,找出三者中的最大值,最大值才能作为新的父结点。当待调整元素以及比其两个孩子都要大时,说明待调整元素以及到了其应该在的位置,堆的结构以及恢复,可以结束调整。
9.查询堆顶元素数据
// 返回类型是堆存储的数据类型 HPDataType HeapTop(HP* php) { // php不可能为空 assert(php); // 当堆中有数据时才能查看堆顶元素数据 assert(php->size > 0); // 返回数组下标为0的数据 return php->a[0]; }
堆的元素是以数组的方式存储的,且堆顶元素就是数组的首元素,所以返回数组首元素的数据即可。
10.查询堆的有效数据个数
int HeapSize(HP* php) { // php不可能为空,值得断言一下 assert(php); // 返回size的值 return php->size; }
由于 size 的值就是堆的有效数据个数,所以我们直接将其返回即可。
11.判断堆是否为空
bool HeapEmpty(HP* php) { // php不可能为空 assert(php); // 如果堆的有效数据个数为0,说明堆为空,返回真,否则返回假 return php->size == 0; }
当堆中无有效数据个数时,size的值肯定为0,否则不为0,可以借此来判断堆是否为空。
五、代码整合及结果演示
1.代码整合
若是在整合后出现某些函数不安全的错误,请在头文件里面加上下面这行代码。
#define _CRT_SECURE_NO_WARNINGS 1
1.头文件 Heap.h 部分
#include<stdio.h> #include<stdlib.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 HeapPush(HP* php, HPDataType x); void HeapPop(HP* php); HPDataType HeapTop(HP* php); int HeapSize(HP* php); bool HeapEmpty(HP* php);
2.源文件 Heap.c 部分
#include"Heap.h" void HeapInit(HP* php) { assert(php); php->a = NULL; php->size = 0; php->capacity = 0; } void HeapDestroy(HP* php) { assert(php); free(php->a); php->a = NULL; php->size = 0; php->capacity = 0; } 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[child] > a[parent]) { Swap(&a[child], &a[parent]); 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 : 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; } php->a[php->size] = x; ++php->size; AdjustUp(php->a, php->size - 1); } void AdjustDown(HPDataType* a, int size) { int parent = 0; int child = parent * 2 + 1; while (child < size) { if (child + 1 < size && a[child] < a[child + 1]) { ++child; } if (a[parent] < a[child]) { Swap(&a[parent], &a[child]); 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); } 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; }
3.源文件 test.c 部分
#include"Heap.h" void Test01() { int arr[] = { 245,7345,123,874,43,78,235,6457,1235,784,34,63,2 }; HP hp; HeapInit(&hp); for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) { HeapPush(&hp, arr[i]); } while (!HeapEmpty(&hp)) { printf("%d ", HeapTop(&hp)); HeapPop(&hp); } printf("\n"); HeapDestroy(&hp); } int main() { Test01(); return 0; }
2.结果演示
1.创建的堆的结构
符合大堆的结构。
2.依次取出的堆顶元素
通过此方法可以解决 Top-K 问题。
总结
本篇文章详细介绍了堆的实现方法,篇幅较长,难免出现纰漏,如果发现问题,欢迎大家指正。如果绝对本篇文章对你有所帮助,还请三连,您的支持是我最大的动力,谢谢。