跳到主要内容数据结构与算法:C++ 堆的实现与原理详解 | 极客日志C++算法
数据结构与算法:C++ 堆的实现与原理详解
综述由AI生成堆的定义与结构,详细阐述了基于数组实现的堆的核心操作,包括初始化、销毁、入堆(向上调整)、出堆(向下调整)、取堆顶及判空功能。通过图文解析了小根堆与大根堆的调整逻辑,并提供了完整的 C++ 堆类源码,适合数据结构与算法学习者参考。
疯疯癫癫31 浏览 

一、堆的定义与结构
堆的本质是一颗完全二叉树,只是它的要求比完全二叉树更加严格。它要求每颗子树的根节点都是当前子树的最大值或最小值。当根节点是最大值时,它就是一个大根堆;当根节点是最小值时,它就是一个小根堆。
在上篇文章中我们也提到了,存储完全二叉树可以使用数组,存储非完全二叉树可以使用链表。而堆就是一种特殊的完全二叉树,所以堆的存储我们就使用数组,也就是顺序表的形式。
我们将堆这个完全二叉树从上至下,从左至右的数据存放在数组中。至于怎么保证它每颗子树的根节点都是当前子树的最大值或最小值,我们在入堆和出堆的位置细讲。顺序表的结构如下:
typedef int HPDataType;
typedef struct Heap {
HPDataType* arr;
int size;
int capacity;
} HP;
二、堆的实现
1. 堆的初始化和销毁
堆的初始化
堆的初始化就是将数组置空,有效数据个数和容量大小置 0。
void HPInit(HP* php) {
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
堆的销毁
堆的销毁就是先判断数组是否为空,不为空就将它释放掉。因为数组的空间是我们向操作系统申请来的,不会主动释放,如果我们不主动释放就会造成内存泄漏。最后我们将数组置空,有效数据个数和容量大小置 0。
void HPDestroy(HP* php) {
assert(php);
if (php->arr)
{
free(php->arr);
}
php->arr = ;
php->size = php->capacity = ;
}
NULL
0
2. 向上调整算法和入堆
接下来就是入堆操作,也就是向堆中插入数据。但是我们要知道,一般往数组中插入数据都是向数组尾部插入,那么是不是就会出现,原本堆的每颗子树的根节点都是当前子树的最大值或最小值,但是从尾部插入数据后会打破这个平衡。
可以看到,原本的堆是一个小根堆,但是我们插入一个 5 之后,它就不构成小根堆了。这个时候就要用到我们的向上调整算法。当然,如果插入一个数据后还依然构成小根堆的话,我们就不做处理即可。
向上调整算法
在讲解向上调整算法时,我们就统一以小根堆为例。向上调整算法的本质就是将我们插入的数据当作孩子节点,让它和它的父节点进行比较。
那么有了孩子节点,怎么找到父节点呢?其实我们在上一篇讲过,父节点 parent 的下标等于 (child - 1) / 2。找到父节点后,我们就看插入的数据是不是比它的父节点还小,如果是那么就直接进行交换,否则就不做操作。
但是我们发现交换一次后还是没有构成小根堆,所以向上调整算法要求,只要我们做了交换,那么就让 child 走到 parent,parent 再走到新的 child 的父节点,继续进行比较,直到 child 为 0,此时它就没有父亲节点了,停止向上调整。
void Swap(HPDataType* p1, HPDataType* p2) {
HPDataType x = *p1;
*p1 = *p2;
*p2 = x;
}
void AdjustUp(HPDataType* arr, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
} else
{
break;
}
}
}
这里是建小根堆的写法,如果想要建大堆,那么将小于改成大于即可。
入堆
有了向上调整算法我们入堆就很简单了,只需要将数据插入到数组最后,然后调用向上调整函数,就可以让我们的堆不被打乱。
但是我们同时要注意,插入数据之前要检查数组空间大小是否足够,如果不够的话要进行扩容。
void HPPush(HP* php, HPDataType x) {
assert(php);
if (php->size == php->capacity) {
php->capacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * php->capacity);
if (NULL == tmp) {
perror("realloc fail");
return;
}
php->arr = tmp;
php->capacity *= 2;
}
php->arr[php->size] = x;
AdjustUp(php->arr, php->size);
php->size++;
}
3. 向下调整算法和出堆顶数据
在正式了解向下调整算法和出堆顶数据之前,首先我们要知道堆删除数据是删除堆顶的数据,也就是下标为 0 的数据。因为堆顶的数据是最特殊的,它是整个堆最大或最小的值,我们在堆的应用会讲到它的用法。
那么了解了这一点之后,我们再来想想怎么删除堆顶数据。如果直接头删的话,那么之前堆的结构会完全乱套。以小根堆为例,我们可以看到如果我们直接对堆进行头删的话,整个堆的数据都被打乱了,结构也变乱了,我们要调整的话也无从下手。
删除堆顶数据的正确做法就是,交换最后一个数据和堆顶数据,然后让 size--,这样我们就只会影响最后一个数据和堆顶数据,不会影响其它节点。
经过上面的操作,我们就可以发现,我们删除了堆顶数据,只是说将堆中的最后一个数据移到了堆顶,但是也只改变了堆中的最后一个数据的位置,不至于像头删那样将整个堆的结构打乱。
那么将堆中的最后一个数据放到了堆顶,此时堆很可能不是一个有效的堆,所以我们需要从堆顶向下调整整个堆,我们需要一个向下调整算法。
向下调整算法
经过上面的分析,我们知道堆删除数据后,堆顶元素可能不符合堆的要求,所以我们要从堆顶开始向下调整。要注意的是,我们举例都是以小根堆为例。
具体方法就是,将堆顶当作父节点 parent,根据 2 * parent + 1 找到它的孩子节点 child,最后让父节点和孩子节点进行比较。如果孩子节点更小就进行交换,然后让父亲走到孩子的位置,孩子再走到新父亲的孩子节点。
如果孩子节点比父节点更大的话就不做修改,跟我们的向上调整算法类似。但是我们要注意一个点,我们在向下调整的时候,需要看当前父节点的左孩子和右孩子谁小,父节点要和小的那个孩子进行交换。为什么呢?
因为如果父节点和较大的那个孩子进行交换的话,较大的那个孩子就成了堆顶,另一个较小的孩子就比堆顶小,不满足小根堆的条件。
所以我们在进行向下调整时,找到左孩子 child 后,还要先判断一下左右孩子谁更小。如果左孩子更小就不需要做更改,如果右孩子更小就让 child++,这样就可以让 child 走到更小的右孩子了(注意左右孩子的关系,右孩子比左孩子的下标大 1)。
那么有了正确的思路之后我们重新走一遍上面的过程,看看有没有问题。
通过一系列的画图分析,我们直接根据思路写出对应的代码即可。
void AdjustDown(HPDataType* arr, int parent, int n) {
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child + 1] < arr[child]) {
child++;
}
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
出堆
上面我们其实已经完整讲解了出堆的过程,这里我们再次回顾一下。出堆就是指删除堆中的堆顶数据,方法就是交换堆顶和最后一个数据,让 size--,间接删除了堆顶数据,然后最后一个数据到了堆顶,再对它进行向下调整即可。
void HPPop(HP* php) {
assert(php);
assert(!HPEmpty(php));
Swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
AdjustDown(php->arr, 0, php->size);
}
4. 堆的有效数据个数和判空
堆的有效数据个数
堆的有效数据个数由 size 记录,直接返回 size 即可。
int HPSize(HP* php) {
assert(php);
return php->size;
}
堆的判空
堆的判空就是判断堆的有效数据个数是否为 0,也是跟 size 相关。
bool HPEmpty(HP* php) {
return php->size == 0;
}
5. 取堆顶数据
HPDataType HPTop(HP* php) {
assert(php);
return php->arr[0];
}
三、堆的源码
#include <stdio.h>
#include <errno.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap {
HPDataType* arr;
int size;
int capacity;
} HP;
#define _CRT_SECURE_NO_WARNINGS 1
void HPInit(HP* php) {
assert(php);
php->arr = NULL;
php->size = php->capacity = 0;
}
void HPDestroy(HP* php) {
assert(php);
if (php->arr)
{
free(php->arr);
}
php->arr = NULL;
php->size = php->capacity = 0;
}
void Swap(HPDataType* p1, HPDataType* p2) {
HPDataType x = *p1;
*p1 = *p2;
*p2 = x;
}
void AdjustUp(HPDataType* arr, int child) {
int parent = (child - 1) / 2;
while (child > 0) {
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
child = parent;
parent = (child - 1) / 2;
} else
{
break;
}
}
}
void HPPush(HP* php, HPDataType x) {
assert(php);
if (php->size == php->capacity) {
php->capacity = php->capacity == 0 ? 4 : 2 * php->capacity;
HPDataType* tmp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * php->capacity);
if (NULL == tmp) {
perror("realloc fail");
return;
}
php->arr = tmp;
php->capacity *= 2;
}
php->arr[php->size] = x;
AdjustUp(php->arr, php->size);
php->size++;
}
void AdjustDown(HPDataType* arr, int parent, int n) {
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && arr[child + 1] < arr[child]) {
child++;
}
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
bool HPEmpty(HP* php) {
return php->size == 0;
}
void HPPop(HP* php) {
assert(php);
assert(!HPEmpty(php));
Swap(&php->arr[0], &php->arr[php->size - 1]);
php->size--;
AdjustDown(php->arr, 0, php->size);
}
int HPSize(HP* php) {
assert(php);
return php->size;
}
HPDataType HPTop(HP* php) {
assert(php);
return php->arr[0];
}
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online