堆的概念实现

前言

本文将详细讲解堆。堆是一种二叉树(一般是完全二叉树)使用顺序结构的数组来存储。
tip:这里我们需要注意区分堆在不同地方的含义,这里的堆是一个数据结构,操作系统虚拟进程地址空间的堆是操作系统中管理内存的一块区域分段。

一、堆的概念及结构

1、堆的概念

如果有一个关键码的集合K,把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足堆中某个节点的值总是不大于(或不小于)其父节点的值,则称为大堆(小堆)。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

tip:

  • 堆总是一颗完全二叉树。
  • 大堆:树中所有父亲都大于或等于孩子。
  • 小堆:树中所有父亲都小于或等于孩子。
  • 注意:堆不一定有序的,因为左右孩子谁大谁小并没有限制。

2、堆的结构

在这里插入图片描述
tip:学习堆我们一定要画图,因为堆在内存中的存储结构是一个数组,但元素之间的逻辑是一颗二叉树,我们很难可以将其想象出来,所以学堆画图很重要

二、堆的实现

虽然堆分为两类,大根堆和小根堆,但是他们的结构与功能都是类似的,所以这里我们实现一个大堆为例就可以了。

我们先预览堆的结构与所需的接口函数:

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//堆——完全二叉树
//虽然堆在内存上是用一段物理地址连续的存储单元依次存储的,但是逻辑关系上是一颗完全二叉树
//堆又分为大根堆和小根堆
//大根堆:树中所有父亲都大于或等于孩子
//小根堆:树中所有父亲都小于或等于孩子
//大根堆和小根堆的实现基本相似,所以这里我们以实现一个大根堆为例typedef int HPDataType;//重命名堆中数据的类型——》优点:①一改全改;②见名知意
typedef struct Heap
{HPDataType* arr;//指向堆区申请的数组int size;//存储有效数据个数int capacity;//堆的容量空间
}HP;//堆的初始化——初始化堆的成员变量
void HeapInit(HP* php);//堆的销毁——动态申请的空间,自己要记得销毁,否则可能造成内存泄漏
void HeapDestroy(HP* php);//交换
void Swap(HPDataType* e1, HPDataType* e2);//堆的向上调整
void AdjustUp(HPDataType* arr, int child);//堆的插入
void HeapPush(HP* php, HPDataType x);//打印堆数组
void HeapPrint(HP* php);//判断堆是否为空
bool HeapIsEmpty(HP* php);//堆的向下调整
void AdjustDown(HPDataType* arr, int n, int parent);//堆的删除
void HeapPop(HP* php);//获取堆顶元素
HPDataType HeapTop(HP* php);//获取堆的有效元素个数
int HeapSize(HP* php);

1、堆的结构

堆由三个成员变量组成,分别是arr指针指向动态申请的数组、size存储堆的有效数据个数、capacity存储堆的容量,所以堆是一个复杂结构,我们将其定义为结构体。

typedef int HPDataType;//重命名堆中数据的类型——》优点:①一改全改;②见名知意
typedef struct Heap
{HPDataType* arr;//指向堆区申请的数组int size;//存储有效数据个数int capacity;//堆的容量空间
}HP;

tip:

  • 建议将堆中存储的数据的类型typedef,typedef 一可以方便我们更改堆的类型,二见名知意。

2、堆的初始化

堆的初始化模块功能:初始化堆对象的三个成员变量

//堆的初始化——初始化堆的成员变量
void HeapInit(HP* php)
{//断言指针的有效性,php不可能为空assert(php);//初始化堆的成员php->arr = (HPDataType*)malloc(sizeof(HPDataType) * 4);if (NULL == php->arr){//扩容失败perror("HeapInit::malloc");return;}php->size = 0;php->capacity = 4;
}

3、堆的销毁

堆的销毁模块功能:清理堆对象使用的资源,并销毁堆对象中动态申请的资源。

//堆的销毁——动态申请的空间,自己要记得销毁,否则可能造成内存泄漏
void HeapDestroy(HP* php)
{assert(php);//释放堆对象申请的动态空间free(php->arr);//free之后php->arr仍指向原来的空间,可能造成非法访问或多次释放,建议置为空php->arr = NULL;php->size = 0;php->capacity = 0;
}

4、堆的插入(重点)

堆的插入模块功能:先在数组尾插入数据,数据再向上调整找到自己在大堆中的位置。

问题1: 堆插入为什么只有一种?

因为堆要满足大根堆或小根堆的性质,头插和任意位置插入会改变堆原本的结构,而尾插不会改变堆原本的结构。

问题2: 已知有一个数组,它是按照大根堆的性质存储的。现在我们在数组尾插入一个数据80,仍想让数组按照大根堆存储,该怎么实现?

  1. 使用向上调整算法实现。
  2. 向上调整算法的思想在这里插入图片描述
    • 将目标节点(孩子结点)与其父节点比较。
    • 如果目标节点大于其父节点,交换两个节点的值,并继续向上比较。
    • 结束向上比较的条件:①如果目标节点小于其父节点,则停止向上比较;②最坏的情况是向上比较到根才结束。
  3. 因为其过程是不断的向上比较,所以将其叫做向上调整算法。

    tip:向上调整的前提是除了目标位置,前面数据构成大堆/小堆。

(1)交换两个数据

因为在堆中许多地方都会交换两个数据,所以我们将其模块化。

//交换——有多个模块需要交换,所以将其模块化
void Swap(HPDataType* e1, HPDataType* e2)
{HPDataType temp = *e1;*e1 = *e2;*e2 = temp;
}

tip:

  • 注意:值传递不会改变形参的改变不会改变实参。

(2)向上调整

注意:向上调整的前提是除了目标位置,前面数据构成大堆/小堆。

//堆的向上调整
void AdjustUp(HPDataType* arr, int child)
{//存储父亲的下标int parent = (child - 1) / 2;//大堆向上调整:当孩子大于父亲时,孩子与父亲交换//向上调整的结束条件://①最坏情况:向上到根才结束,即child = 0(parent<0)。但注意不能用parent < 0作为结束,因为parent = (child - 1) / 2不可能小于0.//②特殊情况:当孩子不大于父亲时结束。while (child > 0){if (arr[child] > arr[parent]){//交换Swap(&arr[child], &arr[parent]);//继续向上调整child = parent;parent = (child - 1) / 2;}else{//跳出循环break;}}
}

tip:

  • 最坏情况是到根结束,注意不能用parent < 0来判断,因为parent = (child - 1)/ 2不可能小于0。
  • 小根堆的向上调整与大根堆类似,只需将if中的arr[child] > arr[parent]改成arr[child] < arr[parent]即可。

(3)堆的插入

//堆的插入
void HeapPush(HP* php, HPDataType x)
{assert(php);//1、向堆插入数据,要先判断是否扩容if (php->size == php->capacity){//扩容//realloc可能扩容失败,所以先使用一个临时变量保存realloc的返回值HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * php->capacity * 2);//判断是否扩容成功if (temp != NULL){//扩容成功,仍然使用php->arr指向申请的空间,并将temp置为空php->arr = temp;temp = NULL;//注意别忘记更新容量php->capacity *= 2;}else{perror("realloc");return;}}//2、堆的插入——先在数组尾插入数据,再向上调整//①数组尾插——不需要挪动,时间复杂度O(1)php->arr[php->size] = x;php->size++;//②向上调整AdjustUp(php->arr, php->size - 1);
}

tip:

  • 注意我们插入数据之前,要检查是否需要扩容。

5、堆的判空

堆的判空模块功能:如果堆为空,则返回真;反之,返回假。

//判断堆是否为空
bool HeapIsEmpty(HP* php)
{assert(php);//当堆中有效个数为0时,堆为空return 0 == php->size;
}

tip:

  • 判断一个变量与一个常量是否相等时,建议变量做右操作数,提高代码的健壮性。

6、堆的删除(重点)

堆的删除模块功能:删除堆顶的元素。

问题1: 堆的删除为什么只能删除堆顶元素?

因为堆的性质,堆顶元素的值最大或最小。

所以删除堆顶元素才有意义,删除掉最大的之后我们可以继续选出第二大的。

问题2: 我们直接删除堆顶?

我们不会直接删除堆顶元素,因为直接删除堆顶元素有两大问题:

  • 数组头删,需要挪动数据,时间复杂度为O(N),效率低。
  • 父子兄弟关系全乱了。在这里插入图片描述

问题3: 怎样删除堆顶?

思路

  1. 交换:把堆顶元素与最后一个元素交换
  2. 删除堆顶:数组尾删,直接size–即可
  3. 向下调整:运用向下调整算法,确保堆的结构。

问题4: 怎样向下调整?

在这里插入图片描述
思路

  1. 通过假设法选出左右孩子较大的那个孩子结点
  2. 将较大的那个孩子结点与父节点比较,如果比父节点大,则交换
  3. 继续向下比较,直到比父节点小才停止,最坏的情况是,向下比较到叶子才停止。

    tip:向下调整的前提是左右子树都是大堆/小堆。

(1)向下调整

注意:向下调整的前提是左右子树都是大堆/小堆。

//堆的向下调整
void AdjustDown(HPDataType* arr, int n, int parent)
{//假设法:定义一个变量存储较大孩子的下标,先假设左孩子大,再通过if确定假设是否成立。int child = parent * 2 + 1;//大堆向下调整:当父亲小于较大的孩子,孩子与父亲交换//向下结束的条件://①最坏情况: 向下到叶子结束,即child(parent * 2 + 1) > n。//②特殊情况:当孩子不小于父亲时结束。while (child < n){//if确定假设是否成立//注意:要先判断child + 1是否为堆有效数据if (child + 1 < n && arr[child] < arr[child + 1]){++child;}if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);//继续向下调整parent = child;child = parent * 2 + 1;}else{break;}}
}

tip:

  • 比较左右孩子的时候,需要先判断右孩子是否为堆的有效数据,当child(parent * 2 + 1) > n,没有右孩子。
  • 小根堆的向下调整与大根堆类似,只需将if中的arr[child] > arr[parent]改成arr[child] < arr[parent]即可。

(2)堆的删除

//堆的删除
void HeapPop(HP* php)
{assert(php);//删除堆不能为空assert(!HeapIsEmpty(php));//堆只有删除堆顶元素才有意义//问题:是直接删吗?//答案是:不是,直接删,有两个问题——①效率低,挪动数据时间复杂度O(N);②堆的父子兄弟关系全乱了//①交换堆顶与堆尾Swap(&php->arr[0], &php->arr[php->size - 1]);//②数组尾删php->size--;//③向下调整AdjustDown(php->arr, php->size, 0);
}

tip:

  • 在删除数据之前,需要判断堆是否为空。
  • 堆删除的思路口诀:一交换二删除三向下调整。

7、获取堆顶元素

//获取堆顶元素
HPDataType HeapTop(HP* php)
{assert(php);//断言堆不为空assert(!HeapIsEmpty(php));return php->arr[0];
}

tip:

  • 为了程序的健壮性,在获取之前断言堆不为空。
  • 获取堆顶元素,直接返回数组下标为0的元素即可

8、获取堆的有效数据个数

//获取堆的有效元素个数
int HeapSize(HP* php)
{assert(php);return php->size;
}

tip:

  • 直接返回size即可。

9、堆的打印

//打印堆数组
void HeapPrint(HP* php)
{assert(php);int i = 0;for (i = 0; i < php->size; ++i){printf("%d ", php->arr[i]);}printf("\n");
}

tip:

  • 堆在内存中是连续存储的其本质就是数组,所以使用for循环就打印了。

三、总代码

1、接口声明模块

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//堆——完全二叉树
//虽然堆在内存上是用一段物理地址连续的存储单元依次存储的,但是逻辑关系上是一颗完全二叉树
//堆又分为大根堆和小根堆
//大根堆:树中所有父亲都大于或等于孩子
//小根堆:树中所有父亲都小于或等于孩子
//大根堆和小根堆的实现基本相似,所以这里我们以实现一个大根堆为例typedef int HPDataType;//重命名堆中数据的类型——》优点:①一改全改;②见名知意
typedef struct Heap
{HPDataType* arr;//指向堆区申请的数组int size;//存储有效数据个数int capacity;//堆的容量空间
}HP;//堆的初始化——初始化堆的成员变量
void HeapInit(HP* php);//堆的销毁——动态申请的空间,自己要记得销毁,否则可能造成内存泄漏
void HeapDestroy(HP* php);//交换
void Swap(HPDataType* e1, HPDataType* e2);//堆的向上调整
void AdjustUp(HPDataType* arr, int child);//堆的插入
void HeapPush(HP* php, HPDataType x);//打印堆数组
void HeapPrint(HP* php);//判断堆是否为空
bool HeapIsEmpty(HP* php);//堆的向下调整
void AdjustDown(HPDataType* arr, int n, int parent);//堆的删除
void HeapPop(HP* php);//获取堆顶元素
HPDataType HeapTop(HP* php);//获取堆的有效元素个数
int HeapSize(HP* php);

2、接口实现模块

#include"Heap.h"//堆的初始化——初始化堆的成员变量
void HeapInit(HP* php)
{//断言指针的有效性,php不可能为空assert(php);//初始化堆的成员php->arr = (HPDataType*)malloc(sizeof(HPDataType) * 4);if (NULL == php->arr){//扩容失败perror("HeapInit::malloc");return;}php->size = 0;php->capacity = 4;
}//堆的销毁——动态申请的空间,自己要记得销毁,否则可能造成内存泄漏
void HeapDestroy(HP* php)
{assert(php);//释放堆对象申请的动态空间free(php->arr);//free之后php->arr仍指向原来的空间,可能造成非法访问或多次释放,建议置为空php->arr = NULL;php->size = 0;php->capacity = 0;
}//交换——有多个模块需要交换,所以将其模块化
void Swap(HPDataType* e1, HPDataType* e2)
{HPDataType temp = *e1;*e1 = *e2;*e2 = temp;
}//堆的向上调整
void AdjustUp(HPDataType* arr, int child)
{//存储父亲的下标int parent = (child - 1) / 2;//大堆向上调整:当孩子大于父亲时,孩子与父亲交换//向上调整的结束条件://①最坏情况:向上到根才结束,即child = 0(parent<0)。但注意不能用parent < 0作为结束,因为parent = (child - 1) / 2不可能小于0.//②特殊情况:当孩子不大于父亲时结束。while (child > 0){if (arr[child] > arr[parent]){//交换Swap(&arr[child], &arr[parent]);//继续向上调整child = parent;parent = (child - 1) / 2;}else{//跳出循环break;}}
}//堆的插入
void HeapPush(HP* php, HPDataType x)
{assert(php);//1、向堆插入数据,要先判断是否扩容if (php->size == php->capacity){//扩容//realloc可能扩容失败,所以先使用一个临时变量保存realloc的返回值HPDataType* temp = (HPDataType*)realloc(php->arr, sizeof(HPDataType) * php->capacity * 2);//判断是否扩容成功if (temp != NULL){//扩容成功,仍然使用php->arr指向申请的空间,并将temp置为空php->arr = temp;temp = NULL;//注意别忘记更新容量php->capacity *= 2;}else{perror("realloc");return;}}//2、堆的插入——先在数组尾插入数据,再向上调整//①数组尾插——不需要挪动,时间复杂度O(1)php->arr[php->size] = x;php->size++;//②向上调整AdjustUp(php->arr, php->size - 1);
}//打印堆数组
void HeapPrint(HP* php)
{assert(php);int i = 0;for (i = 0; i < php->size; ++i){printf("%d ", php->arr[i]);}printf("\n");
}//判断堆是否为空
bool HeapIsEmpty(HP* php)
{assert(php);//当堆中有效个数为0时,堆为空return 0 == php->size;
}//堆的向下调整
void AdjustDown(HPDataType* arr, int n, int parent)
{//假设法:定义一个变量存储较大孩子的下标,先假设左孩子大,再通过if确定假设是否成立。int child = parent * 2 + 1;//大堆向下调整:当父亲小于较大的孩子,孩子与父亲交换//向下结束的条件://①最坏情况: 向下到叶子结束,即child(parent * 2 + 1) > n。//②特殊情况:当孩子不小于父亲时结束。while (child < n){//if确定假设是否成立//注意:要先判断child + 1是否为堆有效数据if (child + 1 < n && arr[child] < arr[child + 1]){++child;}if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);//继续向下调整parent = child;child = parent * 2 + 1;}else{break;}}
}//堆的删除
void HeapPop(HP* php)
{assert(php);//删除堆不能为空assert(!HeapIsEmpty(php));//堆只有删除堆顶元素才有意义//问题:是直接删吗?//答案是:不是,直接删,有两个问题——①效率低,挪动数据时间复杂度O(N);②堆的父子兄弟关系全乱了//①交换堆顶与堆尾Swap(&php->arr[0], &php->arr[php->size - 1]);//②数组尾删php->size--;//③向下调整AdjustDown(php->arr, php->size, 0);
}//获取堆顶元素
HPDataType HeapTop(HP* php)
{assert(php);//断言堆不为空assert(!HeapIsEmpty(php));return php->arr[0];
}//获取堆的有效元素个数
int HeapSize(HP* php)
{assert(php);return php->size;
}

3、功能测试模块

#include"Heap.h"int main()
{//定义堆变量HP hp;//初始化HeapInit(&hp);//插入数据HeapPush(&hp, 6);HeapPush(&hp, 16);HeapPush(&hp, 36);HeapPush(&hp, 56);HeapPush(&hp, -1);HeapPush(&hp, 5);HeapPush(&hp, -16);HeapPush(&hp, 35);HeapPush(&hp, 19);HeapPush(&hp, 9);HeapPush(&hp, 6);HeapPush(&hp, 18);HeapPrint(&hp);//找出前k个大的数int k = 0;scanf("%d", &k);while (!HeapIsEmpty(&hp) && k--){printf("%d ", HeapTop(&hp));//删除HeapPop(&hp);}printf("\n");HeapDestroy(&hp);return 0;
}

运行结果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/462036.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

备战蓝桥杯---动态规划之背包问题引入

先看一个背包问题的简单版&#xff1a; 如果我们暴力枚举可能会超时。 但我们想一想&#xff0c;我们其实不关心怎么放&#xff0c;我们关心的是放后剩下的体积。 用可行性描述即可。 于是我们令f[i][j]表示前i个物品能否放满体积为j的背包。 f[i][j]f[i-1][j]||f[i-1][j-v…

怎么开启耳穴实操的道路

一.背景 前面的文章耳穴为什么有用&#xff1f;-CSDN博客&#xff0c;我说明了耳穴为什么有用、又是很安全的。 然后又告诉你了颈椎痛的耳穴配方用程序员的思维学习耳穴调理的第一个配方“颈椎病”-CSDN博客。 大胆放手去实操吧&#xff01; 二.工具用什么 1.耳穴贴 由一…

K210如何下载程序

一、打开资料包里提供的K-Flash程序烧录软件 二、选择串口 三、选择波特率 四、选择In-Chip&#xff0c;烧录到Flash芯片里面&#xff0c;重新上电还会运行程序 五、如果选择In - Memory&#xff0c;这次可以运行&#xff0c;但下次重新上电就不会保持这次的程序了。 六、选择固…

火星符号运算 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 已知火星人使用的运算符号为 #和$ 其与地球人的等价公式如下 x#y2*x3*y4 x$y3*xy2x y是无符号整数。地球人公式按照c语言规则进行计算。火星人公式中&#xff0…

leetcode:63.不同路径二

dp数组含义&#xff1a;由初始位置到最终位置路径个数 递推公式&#xff1a;如果没有障碍再进行递推公式 初始化&#xff1a;1.若起始位置和终止位置有障碍路径个数为0 2.dp[i][0] 1和dp[0][j] 1的for循环条件都需要加上一个and dp[i][0] 0和and dp[0][j] 0. 3.遍历顺序…

Vue-Vue3 集成编辑器功能

1、安装依赖 编辑器插件需要安装 wangeditor/editor 和 wangeditor/editor-for-vue 两个插件 npm install wangeditor/editor --savevue3运行如下命令安装 npm install wangeditor/editor-for-vuenext --savevue2运行如下命令安装 npm install wangeditor/editor-for-vue -…

【前端web入门第四天】01 复合选择器与伪类选择器

文章目录: 1. 复合选择器 1.1 后代选择器 1.2 子代选择器 1.3 并集选择器1.4 交集选择器(了解) 2.伪类选择器 2.1 伪类-文本2.2 伪类-超链接&#xff08;拓展) 1. 复合选择器 什么叫复合选择器? 由两个或多个基础选择器&#xff0c;通过不同的方式组合而成。 复合选择器的作…

Peter算法小课堂—枚举优化

哈哈哈&#xff0c;新年快乐&#xff01;这一次Peter将要给大家讲一讲轻松、摆烂的算法—枚举&#xff01;咋就是说呀&#xff0c;枚举这个玩意我语法就会了。但大家想想&#xff0c;咱们CSP考试时&#xff08;除了没过初赛的&#xff09;只给1秒&#xff0c;大家想想&#xff…

macOS Sonoma 14.3.1(23D60)发布

系统介绍 黑果魏叔2 月 9 日消息&#xff0c;苹果今日向 Mac 电脑用户推送了 macOS 14.3.1 更新&#xff08;内部版本号&#xff1a;23D60&#xff09;&#xff0c;本次更新距离上次发布隔了 17 天。 魏叔 查询苹果官方更新日志&#xff0c;macOS Sonoma 14.3.1 修复内容和 …

二层交换机配置以太网通道

实验大纲 二层聚合端口配置 1.构建网络拓扑结构图 2.修改交换机名字 3.创建聚合组进入聚合接口模式 4.将端口绑定到聚合端口&#xff08;接口模式&#xff09; 5.聚合接口下端口配置&#xff08;聚合接口模式) 6.具体配置 7.验证端口通道1的状态 8.配置ip 9.测试连通…

Qt安装配置教程windows版(包括:Qt5.8.0版本,Qt5.12,Qt5.14版本下载安装教程)(亲测可行)

目录 Qt5.8.0版本安装教程Qt5.8.0版本下载安装 Qt5.12.2版本安装教程下载安装 Qt 5.14.2安装教程下载安装和创建项目 参考视频 QT为嵌入式系统提供了大量的库和可重用组件。 WPS Office&#xff0c;咪咕音乐&#xff0c;Linux桌面环境等都是QT开发的。 Qt5.8.0版本安装教程 Q…

【C++第二阶段】运算符重载-【+】【cout】【++|--】

你好你好&#xff01; 以下内容仅为当前认识&#xff0c;可能有不足之处&#xff0c;欢迎讨论&#xff01; 文章目录 运算符重载加法运算符重载重载左移运算符递增|减运算符重载 运算符重载 加法运算符重载 What 普通的加减乘除&#xff0c;只能应付C中已给定的数据类型的运…