数据结构与算法:C++ 堆的实现与原理详解
堆的定义与结构,详细阐述了基于数组实现的堆的核心操作,包括初始化、销毁、入堆(向上调整)、出堆(向下调整)、取堆顶及判空功能。通过图文解析了小根堆与大根堆的调整逻辑,并提供了完整的 C++ 堆类源码,适合数据结构与算法学习者参考。

堆的定义与结构,详细阐述了基于数组实现的堆的核心操作,包括初始化、销毁、入堆(向上调整)、出堆(向下调整)、取堆顶及判空功能。通过图文解析了小根堆与大根堆的调整逻辑,并提供了完整的 C++ 堆类源码,适合数据结构与算法学习者参考。



堆的本质是一颗完全二叉树,只是它的要求比完全二叉树更加严格。它要求每颗子树的根节点都是当前子树的最大值或最小值。当根节点是最大值时,它就是一个大根堆;当根节点是最小值时,它就是一个小根堆。
在上篇文章中我们也提到了,存储完全二叉树可以使用数组,存储非完全二叉树可以使用链表。而堆就是一种特殊的完全二叉树,所以堆的存储我们就使用数组,也就是顺序表的形式。
我们将堆这个完全二叉树从上至下,从左至右的数据存放在数组中。至于怎么保证它每颗子树的根节点都是当前子树的最大值或最小值,我们在入堆和出堆的位置细讲。顺序表的结构如下:
typedef int HPDataType;
typedef struct Heap {
HPDataType* arr;
int size;
int capacity;
} HP;
堆的初始化就是将数组置空,有效数据个数和容量大小置 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 = NULL;
php->size = php->capacity = 0;
}
接下来就是入堆操作,也就是向堆中插入数据。但是我们要知道,一般往数组中插入数据都是向数组尾部插入,那么是不是就会出现,原本堆的每颗子树的根节点都是当前子树的最大值或最小值,但是从尾部插入数据后会打破这个平衡。
可以看到,原本的堆是一个小根堆,但是我们插入一个 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;
// child > 0 是循环条件,因为要保证数据是数组元素
while (child > 0) {
// 如果是建小堆,就是 <
if (arr[child] < arr[parent]) {
Swap(&arr[child], &arr[parent]);
// 更新父节点和子节点
child = parent;
parent = (child - 1) / 2;
} else // child >= parent,不用交换
{
break;
}
}
}
这里是建小根堆的写法,如果想要建大堆,那么将小于改成大于即可。
有了向上调整算法我们入堆就很简单了,只需要将数据插入到数组最后,然后调用向上调整函数,就可以让我们的堆不被打乱。
但是我们同时要注意,插入数据之前要检查数组空间大小是否足够,如果不够的话要进行扩容。
// 入堆
void HPPush(HP* php, HPDataType x) {
assert(php);
// 入堆前检查空间,不够就扩容
if (php->size == php->capacity) {
// 判断 capacity 是否相等于 0,不相等就给 2 倍
php->capacity = php->capacity == 0 ? 4 : 2 * php->capacity;
// 使用 realloc 初始化 tmp 数组为 0
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++;
}
在正式了解向下调整算法和出堆顶数据之前,首先我们要知道堆删除数据是删除堆顶的数据,也就是下标为 0 的数据。因为堆顶的数据是最特殊的,它是整个堆最大或最小的值,我们在堆的应用会讲到它的用法。
那么了解了这一点之后,我们再来想想怎么删除堆顶数据。如果直接头删的话,那么之前堆的结构会完全乱套。以小根堆为例,我们可以看到如果我们直接对堆进行头删的话,整个堆的数据都被打乱了,结构也变乱了,我们要调整的话也无从下手。
删除堆顶数据的正确做法就是,交换最后一个数据和堆顶数据,然后让 size--,这样我们就只会影响最后一个数据和堆顶数据,不会影响其它节点。
经过上面的操作,我们就可以发现,我们删除了堆顶数据,只是说将堆中的最后一个数据移到了堆顶,但是也只改变了堆中的最后一个数据的位置,不至于像头删那样将整个堆的结构打乱。
那么将堆中的最后一个数据放到了堆顶,此时堆很可能不是一个有效的堆,所以我们需要从堆顶向下调整整个堆,我们需要一个向下调整算法。
经过上面的分析,我们知道堆删除数据后,堆顶元素可能不符合堆的要求,所以我们要从堆顶开始向下调整。要注意的是,我们举例都是以小根堆为例。
具体方法就是,将堆顶当作父节点 parent,根据 2 * parent + 1 找到它的孩子节点 child,最后让父节点和孩子节点进行比较。如果孩子节点更小就进行交换,然后让父亲走到孩子的位置,孩子再走到新父亲的孩子节点。
如果孩子节点比父节点更大的话就不做修改,跟我们的向上调整算法类似。但是我们要注意一个点,我们在向下调整的时候,需要看当前父节点的左孩子和右孩子谁小,父节点要和小的那个孩子进行交换。为什么呢?
因为如果父节点和较大的那个孩子进行交换的话,较大的那个孩子就成了堆顶,另一个较小的孩子就比堆顶小,不满足小根堆的条件。
所以我们在进行向下调整时,找到左孩子 child 后,还要先判断一下左右孩子谁更小。如果左孩子更小就不需要做更改,如果右孩子更小就让 child++,这样就可以让 child 走到更小的右孩子了(注意左右孩子的关系,右孩子比左孩子的下标大 1)。
那么有了正确的思路之后我们重新走一遍上面的过程,看看有没有问题。
通过一系列的画图分析,我们直接根据思路写出对应的代码即可。
// 向下调整 -- 多传一个 n,是循环的结束条件
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]);
// 交换后,更新 child、parent
parent = child;
child = 2 * parent + 1;
} else {
// 父节点小于子节点
break;
}
}
}
上面我们其实已经完整讲解了出堆的过程,这里我们再次回顾一下。出堆就是指删除堆中的堆顶数据,方法就是交换堆顶和最后一个数据,让 size--,间接删除了堆顶数据,然后最后一个数据到了堆顶,再对它进行向下调整即可。
那么有了思路我们就可以直接写代码了。
// 出堆顶元素
void HPPop(HP* php) {
assert(php);
assert(!HPEmpty(php)); // 堆不为空才出堆
// 删除数据 --- size 是总大小,所以 -1 是最后一个元素的下标
Swap(&php->arr[0], &php->arr[php->size - 1]); // 交换首尾元素
php->size--; // 交换删除后,得到新首尾,向下调整
AdjustDown(php->arr, 0, php->size); // 0 是父节点
}
堆的有效数据个数由 size 记录,直接返回 size 即可。
// 堆的有效数据个数
int HPSize(HP* php) {
assert(php);
return php->size;
}
堆的判空就是判断堆的有效数据个数是否为 0,也是跟 size 相关。
// 判空
bool HPEmpty(HP* php) {
return php->size == 0;
}
取堆顶数据就是取堆中下标为 0 位置的数据。
// 取堆顶元素
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
// #include "Heap.h"
// 初始化
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) {
parent = (child - ) / ;
(child > ) {
(arr[child] < arr[parent]) {
(&arr[child], &arr[parent]);
child = parent;
parent = (child - ) / ;
}
{
;
}
}
}
{
(php);
(php->size == php->capacity) {
php->capacity = php->capacity == ? : * php->capacity;
HPDataType* tmp = (HPDataType*)(php->arr, (HPDataType) * php->capacity);
( == tmp) {
();
;
}
php->arr = tmp;
php->capacity *= ;
}
php->arr[php->size] = x;
(php->arr, php->size);
php->size++;
}
{
child = parent * + ;
(child < n)
{
(child + < n && arr[child + ] < arr[child]) {
child++;
}
(arr[child] < arr[parent]) {
(&arr[child], &arr[parent]);
parent = child;
child = * parent + ;
} {
;
}
}
}
{
php->size == ;
}
{
(php);
(!(php));
(&php->arr[], &php->arr[php->size - ]);
php->size--;
(php->arr, , php->size);
}
{
(php);
php->size;
}
{
(php);
php->arr[];
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online