当前位置: 首页 > 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.lryc.cn/news/580078.html

相关文章:

  • 浏览器中的 preview 和 response 的值不一致和精度问题解决
  • Spring Cloud网关与CI文件配置请求安全性对比
  • MySQL/MariaDB数据库主从复制之基于二进制日志的方式
  • 影楼精修-智能修图Agent
  • Python-将多张图片合并成一张图片调整指定区域的颜色选框工具
  • 应急响应靶场——web3 ——知攻善防实验室
  • 【Unity开发】Unity实现glb模型上传到场景中使用功能
  • 秘塔AI搜索的通过Prompt生成互动式网页探索(二)
  • python脚本编程:使用BeautifulSoup爬虫库获取热门单机游戏排行榜
  • Android发展历程
  • 面试版-前端开发核心知识
  • Oracle如何使用序列 Oracle序列使用教程
  • Java 大视界 -- Java 大数据实战:智能安防入侵检测的特征工程与模型融合全解析
  • 硬件嵌入式学习路线大总结(一):C语言与linux。内功心法——从入门到精通,彻底打通你的任督二脉!
  • Java教程——线程池和future
  • Spring Boot 应用启动时,端口 8080 已被其他进程占用,怎么办
  • 批量PDF转换工具,一键转换Word Excel
  • Jenkins 介绍
  • 后端密码加密:守护用户数据的钢铁长城
  • [尚庭公寓]06-Redis快速入门
  • 通过 Ansys Discovery CFD 仿真探索电池冷板概念
  • Excel 如何进行多条件查找或求和?
  • WPF 右键菜单 MenuItem 绑定图片时只显示最后一个 Icon
  • 深度分析:Microsoft .NET Framework System.Random 的 C++ 复刻实现
  • c# 使用GADL: Can‘t load requested DLL错误处理
  • PixiJS教程(004):点击事件交互
  • gic 中断触发类型
  • Python 中线程和进程在实际项目使用中的区别和联系
  • FastAPI 小白教程:从入门级到实战(源码教程)
  • 基于Docker构建OrangePi5 SDK环境