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

【数据结构入门】堆

目录

1.堆的概念

2.堆的实现(小堆)

2.1 堆的结构体

2.2 堆的初始化&建堆(逻辑)

2.3 建堆(物理数组)

2.3.1 向下调整算法

2.3.2 满足向下调整算法的条件

2.3.3 建堆的时间复杂度分析

2.4 堆排序


1.堆的概念

堆本质上是完全二叉树使用数组存储的一个数据结构。

堆分为大根堆、小根堆:

大根堆

每个父亲结点都大于等于孩子结点

        

小根堆

每个父亲结点都小于等于孩子结点。

        堆可以用来选出这个堆中最大或者最小的值,只需要不断维护这个堆即可,那么后面的堆排序的原理其实就是通过维护堆来实现的。

我们做一下下面的题目,巩固一下堆的概念:

可以直接还原为完全二叉树,也可以目测,首先判断第一个数和第二个数的大小,如果第一个数大于第二个数那么就是大根堆,如果后面的数字有比第一个数字大的直接排除,小根堆同理,选A。

2.堆的实现(小堆)

2.1 堆的结构体

        这里使用数组实现,由于后续可能考虑到堆的插入会涉及到扩容的问题,所以这里需要一个size表示实际的元素个数,以及capacity堆的容量。

typedef int dataType;
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
typedef struct Heap
{dataType* data;int size;int capacity;
}Heap;

2.2 堆的初始化&建堆(逻辑)

        对于堆中的data来说,需要创建一个固定大小的空间,此时外界传入空间的大小,大小赋给size和capacity,需要注意的一点是,第一次建堆的时候,数据是从外界获取的,可以是在数组开辟完毕之后拷贝过来。

void heapInit(Heap* p, dataType* data, int size) 
{p->data = (dataType*)malloc(sizeof(dataType)*size);memcpy(p->data,data, sizeof(dataType) * size);// 提供的数组数据拷贝到堆内数组p->size = size;p->capacity = size;// 建堆
}

        接下来就要开始建堆了,假如外界传入一组数据,我们如何构建小堆呢?

这里举一个非常特殊的例子,当一个完全二叉树的根结点的左右子树都是小堆,只有根结点需要进行调整,那么我们调整的过程如下:

①27首先和自己的左右孩子进行比较,选出最小的孩子和自己交换,即27和15交换。

②接下来,只需要调整左子树即可,因为右子树本身就是一个小堆,我们需要找出27的左右孩子中较小的那一个进行交换,也就是和18进行交换。

        

③最后选出25和27交换,最终形成了小根堆。

上面调整的过程就是所谓的向下调整算法,这个算法的前提是左右子树都是小堆(大堆)

2.3 建堆(物理数组)

①首先需要找到根结点的左右孩子,其实就是根节点的下标*2+1(左孩子),根节点的下标*2+2(右孩子)。

②找到左右孩子之后,需要判断自己是否是小堆,即自己是否比孩子要小,如果不是需要和孩子交换。

③选出孩子中较小的一个进行交换,交换完毕之后,新的根结点就是那个被交换的孩子,以此类推继续判断自己是不是小堆。

④每次计算孩子下标的时候,需要判断下标是否越界,如果越界说明,本节点就是叶子结点。

2.3.1 向下调整算法

        首先函数需要三个参数,第一个是需要调整的堆,第二个是堆中数组的长度,第三个是指定需要调整的堆的根结点的下标。

        然后我们假设根结点的左孩子是最小的孩子,如果判断右孩子比左孩子还小,那么更新右孩子为左孩子。

        其次,若根节点比最小的孩子还要大,那么就进行交换,交换完毕之后需要更新最新的根结点是最小的孩子,同时需要计算最新的孩子。

        最后,我们发现这个过程是一个持续的过程,所以写成循环,退出循环的条件是当孩子节点计算的值大于或等于数组的长度,说明这个结点就是叶子结点需要终止循环;

        然而我们发现循环还是有问题,因为只有头结点交换了才会更新头结点和孩子节点的下标,这样就永远无法退出循环,所以如果没有发生交换,也就是说头结点本身比最小的孩子节点还要小,说明就不需要进行堆调整了,直接退出循环

void Swap(dataType* p1, dataType* p2)
{dataType tmp = *p1;*p1 = *p2;*p2 = tmp;
}// 向下调整算法,默认左右子树都是小堆
void adjustDown(dataType* a, int n,int root)// 堆,数组长度,需要调整的根结点
{// 选出最小的孩子int parent = root;// 最小孩子下标int childIndex = 2 * parent + 1;// 默认左孩子是最小的孩子while (childIndex < n ) //左右孩子都不能越界{// 右孩子更小,那么就调整最小孩子的下标为右孩子if ( childIndex + 1 < n && a[childIndex] > a[childIndex + 1]){++childIndex;}if (a[parent] > a[childIndex]) // 父结点比子结点大,不是小堆,需要交换{Swap(&a[parent], &a[childIndex]);parent = childIndex;childIndex = 2 * parent + 1;}else {// 小的孩子已经大于父亲,直接退出循环即可break;}}
}

        BUG:仔细阅读代码会发现,这个代码存在越界问题,若当前根节点是绿色方框框选的结点,那么我们势必要访问它的孩子节点,此时循环判断左孩子节点(默认最小孩子赋值为左孩子)的下标小于n,但是要访问它的右孩子一定会发生越界的问题,我们该如何处理呢?

while (childIndex < n)
{
// 右孩子更小,那么就调整最小孩子的下标为右孩子if (a[childIndex] > a[childIndex + 1]){++childIndex;}
......
}

这里只有一个左孩子,根结点还是需要和左孩子交换的,所以只需要在判断语句中判断,如果右节点的下标小于数组长度 并且右节点小于左节点,才能更新最小孩子节点为右节点。

while (childIndex < n ) //左右孩子都不能越界
{// 右孩子更小,那么就调整最小孩子的下标为右孩子if ( childIndex + 1 < n && a[childIndex] > a[childIndex + 1]){++childIndex;}.......
}

        到了这里还是有一个致命的问题,我们无法保证传过来的根结点的左右子树都是小堆,这个条件太苛刻了,如果不满足这个条件,就无法使用向下调整算法。

2.3.2 满足向下调整算法的条件

        已知每一个单独的叶子节点都可以看做一个堆,那么我们只需要从叶子结点所处的最小二叉树入手,即找到叶子结点的父结点,(下标-1)/2即可;

        找到父结点之后,例如下图的72,由于两个叶子结点都是单独的节点也就是单独的堆,是满足向下调整算法的条件的,那么我们只需要将72作为根节点进行向下调整算法,使其变成一个堆,以此类推,调完72之后,再去调它的兄弟节点53,只需要下标-1即可,两个子树都符合向下调整的算法,53所在的子树调整完毕之后,下标继续-1,就是48所在的子树进行调整

调整的顺序如下:

所以堆的初始化已经完成,这里可以打印数组看看。

// 堆初始化
void heapInit(Heap* p, dataType* data, int size)
{p->data = (dataType*)malloc(sizeof(dataType) * size);memcpy(p->data, data, sizeof(dataType) * size);// 提供的数组数据拷贝到堆内数组p->size = size;p->capacity = size;// 建堆// 最后一个叶子结点的下标是size - 1,找父亲节点就是,(孩子节点-1)/2for (int i = (size-1-1)/2; i >= 0; --i){adjustDown(p->data,p->size,i); // 最后一个叶子节点的父结点开始每次下标-1,依次进行向下调整}
}

将调整后的数组直接打印,发现能够正常输出小堆。

2.3.3 建堆的时间复杂度分析

        树的高度是\log_2 N,那么最坏情况是要遍历整个堆,一共调整的次数是N/2次,所以可能读者会认为建堆的过程的时间复杂度是N*\log_2 N,但是实际上并不是这样。

for (int i = (size-1-1)/2; i >= 0; --i)
{adjustDown(p->data,p->size,i); // 最后一个叶子节点的父结点开始每次下标-1,依次进行向下调整
}

        因为我们的调整是从堆的最后一个节点的父结点开始的,如果是从0开始那么时间复杂度才是N*\log_2 N,       。

未完待续......

2.4 堆排序

        学会向下调整、建堆之后,我们就可以尝试完成堆排序了。

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

相关文章:

  • powerbi本地报表发布到web,以得到分享链接
  • C99中的变长数组(VLA)
  • 什么是 Spring MVC?
  • 中扬立库与西门子深化技术协同 共绘智能仓储创新蓝图
  • clean install 和 clean package 的区别
  • JVM学习笔记-----图解方法执行流程
  • 百胜软件×华为云联合赋能,“超级国民品牌”海澜之家新零售加速前行
  • 【力扣494】目标和
  • 【软考中级网络工程师】知识点之 IP QoS 技术
  • JVM宝典
  • 面试八股之从Java到JVM层面深入解析ReentrantLock实现原理
  • 力扣top100(day01-05)--矩阵
  • 开放原子开源生态大会:麒麟信安加入openEuler社区AI联合工作组,聚焦操作系统开源实践与行业赋能
  • Linux下的软件编程——文件IO
  • Openlayers基础教程|从前端框架到GIS开发系列课程(24)openlayers结合canva绘制矩形绘制线
  • 循环神经网络
  • THCV215一种高速视频数据收发器,采用低电压差分信号(LVDS)技术支持高速串行数据传输,支持1080p/60Hz高分辨率传输
  • 【[特殊字符][特殊字符] 协变与逆变:用“动物收容所”讲清楚 PHP 类型的“灵活继承”】
  • Gradle(二)Gradle的优势、项目结构介绍
  • 电商双11美妆数据分析(一)
  • Honeywell霍尼韦尔A205压力传感器HC41H106P060169419G固瑞克117764美国制造
  • Rust 项目编译故障排查:从 ‘onnxruntime‘ 链接失败到 ‘#![feature]‘ 工具链不兼容错误
  • KAQG:一种用于生成难度可控问题的知识图谱的增强的RAG系统(论文大白话)
  • 2025AI行业升级生态战:谁在“种树”?谁在“造林”?
  • 02-Ansible 基本使用
  • Visual Studio中VC++目录、C/C++和链接器配置的区别与最佳实践
  • Minst手写数字识别
  • python2操作neo4j
  • 非凸科技受邀参加Community Over Code Asia 2025 Rust分论坛
  • 上海AI实验室发布MinerU2:通专融合路线如何补齐AI-Ready数据的最后一公里