当前位置: 首页 > news >正文

数据结构 之 【堆】(堆的概念及结构、大根堆的实现、向上调整法、向下调整法)(C语言实现)

目录

1.堆的概念及结构

1.1堆的概念

1.2堆的结构 

 2.堆的实现(以大堆为例)

2.1 前提准备

2.2初始化

2.3销毁及交换函数

2.4插入及向上调整法

向上调整法 

2.5删除及向下调整法

向下调整法 

2.6判空、堆顶元素、元素个数


这里的堆和操作系统虚拟进程地址空间中的堆是两回事,

一个是数据结构,一个是操作系统中管理内存的一块区域分段

1.堆的概念及结构

1.1堆的概念

一棵父亲节点的值总是不大于其子节点值的完全二叉树叫作小根堆(最小堆)

一棵父亲节点的值总是不小于其子节点值的完全二叉树叫作大根堆(最大堆),

(后续简称小堆、大堆)

1.2堆的结构 

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费

而完全二叉树适合使用顺序结构存储,因为其每层节点从左至右连续排列,无间隔缺失

 

堆既然是一种完全二叉树,那就可以使用顺序结构的数组进行存储,即

按照从上至下从左至右的数组顺序对所有节点的值进行存储

这样,既不浪费空间,又能够高效率的遍历数据

由上图我们可以看到数组结构可以很好的模拟完全二叉树 

注意:

逻辑结构是指我们想象出来的结构,物理结构是指数据在内存中实际存储时的结构 

堆只要求了父亲节点与子节点值之间的关系,并没要求兄弟节点、堂兄弟节点值之间的关系

 2.堆的实现(以大堆为例)

前面讲了二叉树的顺序存储结构更适用于完全二叉树,

所以实现堆就是在操作数组的基础上实现堆的性质

2.1 前提准备

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>typedef int HpDateType;
typedef struct heap
{HpDateType* a;int size;int capacity;}HP;

1.包含相关头文件;2.为了便于修改数据类型重命名一下

3.定义结构体,在堆上开空间来存储相关数据

2.2初始化

//初始化
void HpInit(HP* php)
{//php指向一个堆,不能为空assert(php);php->a = (HpDateType*)malloc(sizeof(HpDateType) * 4);if (php->a == NULL){perror("malloc fail");return;}php->size = 0;php->capacity = 4;
}void HpInitArray(HP* php, HpDateType* a, int n)
{assert(php);php->a = (HpDateType*)malloc(sizeof(HpDateType) * n);if (php->a == NULL){perror("malloc fail");return;}memmove(php->a, a, n * sizeof(HpDateType));php->size = n;php->capacity = n;//建堆for (int i = (n - 2) / 2; i >= 0; --i){AdjustDown(php->a, php->size, i);}
}

 (1)无参初始化目的是预开一定的空间,

利用给定数组初始化目的是直接根据数组数据进行建堆(后续讲解)

(2)所给的堆结构体指针变量不应为空,所以断言一下

2.3销毁及交换函数

void HpDestroy(HP* php)
{assert(php);free(php->a);php->a = NULL;php->capacity = php->size = 0;
}
//交换
void Swap(HpDateType* p1, HpDateType* p2)
{HpDateType temp = *p1;*p1 = *p2;*p2 = temp;
}

(1)销毁时需正确释放空间并更新指针及其余变量 

(2)后续频繁使用交换操作,这里单独设定一个函数

2.4插入及向上调整法

void HpPush(HP* php, HpDateType x)
{assert(php);//扩容if (php->capacity == php->size){HpDateType* temp = (HpDateType*)realloc(php->a, sizeof(HpDateType) * php->capacity * 2);if (temp == NULL){perror("malloc fail");return;}php->a = temp;php->capacity *= 2;}//实现插入,插入到末尾,实现大堆//需要依次与父亲进行比较,即向上调整php->a[php->size++] = x;AdjustUp(php->a, php->size - 1);
}

(1)插入数据第一步,思考扩容

(2)顺序存储结构中,尾插效率更高,先将数据插入到尾部

(3)为了实现大堆,需要将所插入的数据进行调整,即AdjustUp();

向上调整法 

向上调整法的前提是数据插入前本身就是一个堆

逻辑结构上

如果所插入数据比其祖先节点的值大就进行交换,否则就不动,最终实现大堆

具体操作体现在物理结构,即数组上

通过下标间的关系,parent = ( child - 1 ) / 2 (计算机整除运算自动向下取整)

依次找到尾节点的祖先节点,然后比较数据大小,大就交换,小就停止交换

    int parent = (child - 1) / 2;
while (){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}

这显然是一个循环!

通过前面的分析,我们很容易知道, 情况最坏是,所插入数据需要与根节点的值进行交换,自然地,就会把 parent(父亲节点下标)大于等于0作为循环结束条件,但正如上述代码所展示的一样,通过迭代更新父子节点的下标, parent恒大于等于0(最坏情况当child等于0时也成立),也就是说如果循环内部不能控制住结束条件,这将是一个 死循环
观察到, 所插入数据与根节点的值进行交换后,child 更新为 0 , parent仍等于0                  那么就将       child  > 0      作为循环结束条件
void AdjustUp(HpDateType* a, int child)
{//给定数组为空时,没必要进行下面步骤assert(a);int parent = (child - 1) / 2;//parent恒大于等于0while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}
}

2.5删除及向下调整法

void HpPop(HP* php)
{assert(php);//assert(!HpEmpty());//删除数据,只删除堆顶的数据Swap(&php->a[0], &php->a[php->size - 1]);php->size--;//向下调整,实现大堆AdjustDown(php->a, php->size, 0);
}

(1)数组中,尾删的效率较高,其余位置的删除操作都会因为移动数据造成效率低下

(2)堆的删除操作是针对堆顶元素的删除,原因如下:

在大堆中,最大元素位于堆顶,删除之后通过向下调整法(后续讲解) 次大元素就会位于堆顶

这样的删除更具功能性,更有利于在现有基础上直接找到最大元素

(3)具体操作就是先将堆顶元素与最后一个数据元素进行交换,然后删除堆顶元素,

最后对余下数据进行向下调整操作,使其重新成为一个大堆

向下调整法 

向下调整法的前提是所调整节点的左右子树是一个堆

逻辑结构上

如果该节点数据比其孩子节点中的较大值小就进行交换,否则就不动,最终实现大堆

具体操作体现在物理结构,即数组上

通过下标间的关系,leftchild = parent * 2 + 1 (计算机整除运算自动向下取整)

rightchild = leftchild + 1,依次找到该节点的孩子节点

然后与孩子节点中的较大值进行比较,大就交换,小就停止交换

int child = parent * 2 + 1;while ()
{//左孩子一个逻辑,右孩子一个逻辑,直接假设//child是左右孩子中较小的一个孩子的下标//注意右孩子的有无(防止越界访问与无效数据)if (child + 1 < n && a[child] > a[child + 1]){++child;}//与左右孩子中较大的一个孩子进行比较if (a[parent] > a[child]){Swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;}
}

这显然是一个循环!

向下调整时,

(1)父亲节点有孩子节点时,就需要与孩子节点的值进行一次比较,所以 左孩子节点的下标小于元素个数,即 child < n 可作为循环结束条件

(2)交换操作的实质是如果父亲节点值比孩子节点中的较大值小,就进行交换,否则就不动

那么我们就先假设左孩子节点的值较大,再将左孩子节点的值与右孩子节点值进行比较,更新孩子节点,然后与父亲节点的值进行比较,从而简化比较交换操作

需要进行比较然后进行相同的操作时,先假设再判断更新的方法更好

(3)注意右孩子的有无(防止越界访问与无效数据)

if (child + 1 < n && a[child] > a[child + 1])

必须先判断右孩子的有无,再进行访问操作,否则就是越界访问或无效数据!

void AdjustDown(HpDateType* a, int n, int parent)
{int child = parent * 2 + 1;//有左孩子才进行调整while (child < n)//n是节点个数{//左孩子一个逻辑,右孩子一个逻辑,直接假设//child是左右孩子中较小的一个孩子的下标//注意右孩子的有无(防止越界访问与无效数据)if (child + 1 < n && a[child] > a[child + 1]){++child;}//与左右孩子中较大的一个孩子进行比较if (a[parent] > a[child]){Swap(&a[parent], &a[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}

2.6判空、堆顶元素、元素个数

//判空
bool HpEmpty(HP* php)
{assert(php);return php->size == 0;
}
//堆顶元素
HpDateType HpTop(HP* php)
{//assert(php);//一举两得assert(!HpEmpty(php));return php->a[0];
}
//大小
int HpSize(HP* php)
{assert(php);return php->size;
}

(1)注意相关断言

(2)根据意义直接返回相应值即可

http://www.dtcms.com/a/266549.html

相关文章:

  • K8s服务发布基础
  • CI/CD持续集成与持续部署
  • 基于大模型的强直性脊柱炎全周期预测与诊疗方案研究
  • 力扣面试150(15/150)
  • 7.4 arm作业
  • 玩转n8n工作流教程(一):Windows系统本地部署n8n自动化工作流(n8n中文汉化)
  • 全平台兼容+3倍加载提速:GISBox将重新定义三维可视化标准
  • Java 实现excel大批量导出
  • 什么是金字塔思维?
  • 三体融合实战:Django+讯飞星火+Colossal-AI的企业级AI系统架构
  • RK-Android11-系统增加一个属性值
  • 【HDMI CEC】 设备 OSD 名称功能详解
  • 《设计模式之禅》笔记摘录 - 3.工厂方法模式
  • 【modbus学习笔记】Modbus协议解析
  • WPF学习(四)
  • 分布式集合通信--学习笔记
  • ComfyUI工作流:一键换背景体验不同场景
  • 如何搭建 OLAP 系统?OLAP与数据仓库有什么关系?
  • 2-2 PID-代码部分
  • Fiddler 中文版怎么配合 Postman 与 Wireshark 做多环境接口调试?
  • Hawk Insight|美国6月非农数据点评:情况远没有看上去那么好
  • 如何将FPGA设计验证效率提升1000倍以上(2)
  • 应急响应靶场——web2——知攻善防实验室
  • 大带宽服务器中冗余技术的功能
  • 新能源汽车功率级测试自动化方案:从理论到实践的革命性突破
  • Python常用医疗AI库以及案例解析(2025年版、上)
  • Nginx + ModSecurity + OWASP CRS + Lua + GEOIP2 构建传统WAF
  • 【ACP】阿里云云计算高级运维工程师--ACP
  • 服务器的IO性能怎么看?
  • 【机器学习3】机器学习(鸢尾花分类)项目核心流程与企业实践差异分析