堆的概念、堆的向下调整算法、堆的向上调整算法、堆的基本功能实现

目录

堆的介绍

堆的概念

堆的性质

堆的结构

堆的向下调整算法

基本思想(以建小堆为例)

 代码

堆的向上调整算法

基本思想(以建小堆为例)

代码

 堆功能的实现

堆的初始化 HeapInit

销毁堆 HeapDestroy

打印堆 HeapPrint

堆的插入 HeapPush

堆的删除 HeapPop

获取堆顶的数据 HeapTop

获取堆的数据个数 HeapSize

堆的判空 HeapEmpty


堆的介绍

堆的概念

        堆:如果有一个关键码的集合K={k0,k1,k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足ki<=k2i+1且ki<=k2i+2(或满足ki>=k2i+1且ki>=k2i+2),其中i=0,1,2,…,则称该集合为堆。
        小堆:将根结点最小的堆叫做小堆,也叫最小堆或小根堆。
        大堆:将根结点最大的堆叫做大堆,也叫最大堆或大根堆。

堆的性质

        堆中某个结点的值总是不大于或不小于其父结点的值。
        堆总是一棵完全二叉树。

堆的结构

堆的向下调整算法

        现在我们给出一个数组,逻辑上看作一棵完全二叉树。我们通过从根节点开始的向下调整算法可以把它调整成一个小堆。

但是,使用向下调整算法需要满足一个前提
        若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
        若想将其调整为大堆,那么根结点的左右子树必须都为大堆。 

基本思想(以建小堆为例)

        1.从根结点处开始,选出左右孩子中值较小的孩子。
        2.让小的孩子与其父亲进行比较。
        若小的孩子比父亲还小,则该孩子与其父亲的位置进行交换。并将原来小的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
        若小的孩子比父亲大,则不需处理了,调整完成,整个树已经是小堆了。

 代码

// 定义一个交换函数,用于交换两个整数变量的值
void Swap(int* x, int* y)
{// 创建一个临时变量存储x指向的值int tmp = *x;// 将y指向的值赋给x指向的位置*x = *y;// 将临时变量tmp的值(原x的值)赋给y指向的位置*y = tmp;
}// 定义一个堆的向下调整函数(针对小顶堆)
void AdjustDown(int* a, int n, int parent)
{// 初始化child为当前节点的左孩子的下标,假设左孩子初始时值较小int child = 2 * parent + 1;// 当孩子节点下标小于数组长度时,循环执行以下操作while (child < n){// 如果右孩子存在,并且右孩子的值小于左孩子的值if (child + 1 < n && a[child + 1] < a[child]){// 更新child为较小孩子的下标(即右孩子)child++;}// 如果孩子(左右中较小的一个)的值小于其父节点的值if (a[child] < a[parent]){// 使用交换函数交换父节点和较小的孩子节点的值Swap(&a[child], &a[parent]);// 更新parent为刚交换后较小孩子的下标,准备检查新的子树是否满足堆性质parent = child;// 重新计算下一个待检查的孩子节点的下标child = 2 * parent + 1;}else // 如果当前节点以及其子节点均满足堆性质,则结束调整{break;}}
}

        使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。 

        上面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?
        答案很简单,我们只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可。

// 建堆过程
for (int i = (n - 1 - 1) / 2; i >= 0; i--) 
{// 从最后一个非叶子节点开始,依次对每个节点调用向下调整函数// 这样做可以确保整个堆结构满足小顶堆的性质AdjustDown(php->a, php->size, i);
}

 在这段代码中:

  • `n` 表示堆中的元素个数。
  • 通过 `(n - 1 - 1) / 2` 计算得到最后一个非叶子节点的索引。
  • 遍历从最后一个非叶子节点到根节点(索引为0),依次对每个节点执行向下调整操作,使得每个节点及其子树构成一个小顶堆。
  • `php->a` 是指向堆数组的指针,`php->size` 是堆的大小(元素个数)。
  • 调用 `AdjustDown` 函数逐个对每个父节点进行调整,以保证堆的特性。当遍历完成后,整个数组便构成了一个小顶堆。

堆的向上调整算法

        当我们在一个堆的末尾插入一个数据后,需要对堆进行调整,使其仍然是一个堆,这时需要用到堆的向上调整算法。

基本思想(以建小堆为例)

        1.将目标结点与其父结点比较。

        2.若目标结点的值比其父结点的值小,则交换目标结点与其父结点的位置,并将原目标结点的父结点当作新的目标结点继续进行向上调整。若目标结点的值比其父结点的值大,则停止向上调整,此时该树已经是小堆了。

代码

// 定义一个交换函数,用于交换两个自定义类型 HPDataType 指针所指向的数据
void Swap(HPDataType* x, HPDataType* y)
{// 创建一个临时变量存储x指向的数据HPDataType tmp = *x;// 将y指向的数据赋给x指向的位置*x = *y;// 将临时变量tmp的数据(原x的数据)赋给y指向的位置*y = tmp;
}// 定义一个堆的向上调整函数(针对小顶堆)
void AdjustUp(HPDataType* a, int child)
{// 根据孩子节点的下标计算其父节点的下标int parent = (child - 1) / 2;// 当孩子节点的下标大于0(即未到达根节点)时,循环执行以下操作while (child > 0){// 如果孩子节点的值小于其父节点的值if (a[child] < a[parent]){// 使用交换函数交换孩子节点和父节点的数据Swap(&a[child], &a[parent]);// 更新孩子节点为刚刚交换后的父节点,以便继续向上调整child = parent;// 重新计算新的父节点下标parent = (child - 1) / 2;}else // 如果孩子节点与其父节点的值关系已经满足堆的性质,则停止调整{break;}}
}

在上述代码中:

  • HPDataType 是用户自定义的数据类型,这里假设它支持 < 运算符用于比较大小。
  • AdjustUp 函数主要用于将新插入或更新后的元素调整到正确位置,以保持小顶堆的性质。从下标为 child 的节点开始,如果其值小于父节点,则两者交换位置,直到无法继续交换(即已达到根节点或者不再违反堆的性质)。

 堆功能的实现

堆的初始化 HeapInit

        首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组、堆中元素的个数以及当前堆的最大容量。

// 数据类型定义
typedef int HPDataType; // 定义堆中存储数据的类型为整型// 结构体定义
typedef struct Heap
{// 堆中存储数据的数组,动态分配内存来存储堆中的元素HPDataType* a;// 记录堆中已有元素的个数int size;// 记录堆的最大容量,即数组a可容纳的元素数量int capacity;
} HP; 

        创建完堆类型后,我们还需要一个初始化函数,对刚创建的堆进行初始化,注意在初始化期间要将传入数据建堆。

// 定义一个初始化堆的函数
void HeapInit(HP* php, HPDataType* a, int n)
{// 断言检查传入的堆结构指针是否有效assert(php);// 动态申请一块足够容纳n个HPDataType类型数据的内存空间HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType)*n);// 检查内存申请是否成功if (tmp == NULL){printf("动态内存分配失败!\n");exit(-1); // 若分配失败,程序终止运行}// 将新申请的内存空间赋值给堆结构的数组成员php->a = tmp;// 使用memcpy函数将输入数组a中的数据复制到堆结构的数组中memcpy(php->a, a, sizeof(HPDataType)*n);// 设置堆的已有元素个数为输入的nphp->size = n;// 同时设置堆的容量也为nphp->capacity = n;// 初始化堆:从最后一个非叶子节点开始,依次对每个节点进行向下调整操作int i = 0;for (i = (php->size - 1 - 1) / 2; i >= 0; i--){AdjustDown(php->a, php->size, i);}
}

        此函数的作用是初始化一个堆结构,并根据传入的数据构建一个小顶堆。首先动态分配内存以存储堆中的数据,然后将输入数组a中的数据复制到堆结构的数组中。接着设置堆的大小和容量,并从最后一个非叶子节点开始,使用 AdjustDown 函数对每一个节点进行调整,最终使整个数据结构满足小顶堆的性质。 

销毁堆 HeapDestroy

        为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的。

// 定义一个销毁堆的函数
void HeapDestroy(HP* php)
{// 断言检查传入的堆结构指针是否有效assert(php);// 释放之前动态分配给堆结构数组的空间free(php->a);// 置堆结构的数组指针为空,防止野指针问题php->a = NULL;// 将堆中元素个数清零php->size = 0;// 将堆的容量也清零php->capacity = 0;
}

        此函数用于销毁堆结构,释放堆占用的内存资源并将相关状态信息重置为初始状态,以便后续可能的再次初始化或避免产生内存泄漏等问题。 

打印堆 HeapPrint

        打印堆中的数据,这里用了两种打印格式。第一种打印格式是按照堆的物理结构进行打印,即打印为一排连续的数字。第二种打印格式是按照堆的逻辑结构进行打印,即打印成树形结构。

// 计算具有n个节点的完全二叉树的深度
int depth(int n)
{assert(n >= 0);if (n > 0){int m = 2;int height = 1;while (m < n + 1){m *= 2;height++;}return height;}else{return 0;}
}// 打印堆
void HeapPrint(HP* php)
{assert(php);// 按照数组形式打印堆内容int i = 0;for (i = 0; i < php->size; i++){printf("%d ", php->a[i]);}printf("\n");// 按照树形结构打印堆内容int tree_depth = depth(php->size);int total_nodes = pow(2, tree_depth) - 1; // 获取对应深度的满二叉树的节点总数int current_spaces = total_nodes - 1; // 记录每一行前面的空格数int current_row = 1; // 当前行数int index = 0; // 待打印数据的下标while (true){// 打印前面的空格for (int i = 0; i < current_spaces; i++){printf(" ");}// 打印数据和间距int nodes_in_row = pow(2, current_row - 1); // 每一行的数字个数while (nodes_in_row--){printf("%02d", php->a[index++]); // 打印数据if (index >= php->size) // 如果所有数据都已打印,则结束打印{printf("\n");return;}int spaces_between_numbers = (current_spaces + 1) * 2; // 两个数之间的空格数for (int j = 0; j < spaces_between_numbers; j++) // 打印两个数之间的空格{printf(" ");}}printf("\n"); // 换行current_row++; // 下一行current_spaces = current_spaces / 2 - 1; // 更新当前行的空格数}
}

这段代码实现了两个功能:

  • depth 函数用于计算具有n个节点的完全二叉树的深度。

  • HeapPrint 函数用于打印堆的内容。首先按数组顺序打印堆的所有元素,然后按照树形结构打印堆,使其看起来像一棵二叉树。通过计算堆对应的完全二叉树的深度,确定每一层节点的数量及相应空格数,从而实现树形结构的打印。

堆的插入 HeapPush

        数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构。

// 插入元素到堆中
void HeapPush(HP* php, HPDataType x)
{// 断言检查堆结构指针有效性assert(php);// 判断堆是否已满,如果满了则尝试扩大容量if (php->size == php->capacity){// 申请两倍于当前容量的新内存空间HPDataType* tmp = (HPDataType*)realloc(php->a, 2 * php->capacity * sizeof(HPDataType));// 检查内存分配是否成功if (tmp == NULL){printf("动态内存扩充失败!\n");exit(-1); // 分配失败则退出程序}// 成功分配内存后,更新堆结构的数组指针和容量值php->a = tmp;php->capacity *= 2;}// 将新元素添加至堆数组末尾php->a[php->size] = x;// 堆大小加一php->size++;// 调整堆结构,确保新插入元素后的堆仍满足堆属性AdjustUp(php->a, php->size - 1); // 从新增节点开始向上调整
}

        在这个函数中,我们首先检查堆是否已满,若满则通过realloc函数扩大堆容量。接着将新元素添加至堆数组的末尾,并增加堆的大小计数。最后调用`AdjustUp`函数对新插入的元素进行上浮调整,确保堆仍然满足小顶堆的性质。

堆的删除 HeapPop

        堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整。

        原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为O(N)。

        若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为 O(log(N))。

// 删除堆顶元素
void HeapPop(HP* php)
{// 断言检查堆结构指针有效性assert(php);// 断言检查堆是否为空,如果不是空堆才能进行删除操作assert(!HeapEmpty(php));// 将堆顶元素(即数组的第一个元素)与堆的最后一个元素交换Swap(&php->a[0], &php->a[php->size - 1]);// 删除堆的最后一个元素,即将堆大小减一php->size--;// 由于堆顶元素可能不再满足堆的性质,因此需要对新的堆顶元素进行向下调整操作,以恢复堆的有序性AdjustDown(php->a, php->size, 0);
}

        此函数首先确认堆不为空,然后通过交换堆顶元素和堆尾元素的位置,实际上将堆尾元素移到了堆顶。接着减少堆的大小表示删除了堆顶元素,最后调用 `AdjustDown` 函数从新的堆顶元素开始向下调整堆,确保剩余元素重新形成符合堆性质的结构。

获取堆顶的数据 HeapTop

        获取堆顶的数据,即返回数组下标为0的数据。

// 获取堆顶元素的值
HPDataType HeapTop(HP* php)
{// 断言检查堆结构指针有效性assert(php);// 断言检查堆是否为空,非空堆才能获取堆顶数据assert(!HeapEmpty(php));// 返回堆顶元素的值,即堆数组的第一个元素return php->a[0];
}

        此函数用于安全地获取堆顶元素的值,在确保堆非空的情况下直接返回堆顶元素(小顶堆中最小的元素)。若堆为空,则会触发断言错误,提示堆为空无法获取堆顶元素。 

获取堆的数据个数 HeapSize

        获取堆的数据个数,即返回堆结构体中的size变量。

// 获取堆中元素个数
int HeapSize(HP* php)
{// 断言检查堆结构指针有效性assert(php);// 返回堆中当前存储的数据个数return php->size;
}
此函数用于获取堆中实际包含的元素数量,只需直接返回堆结构体中的 `size` 成员变量即可。在调用此函数前,需确保堆结构指针有效。

堆的判空 HeapEmpty

        堆的判空,即判断堆结构体中的size变量是否为0。

// 判断堆是否为空
bool HeapEmpty(HP* php)
{// 断言检查堆结构指针有效性assert(php);// 如果堆中数据个数为0,则认为堆为空return php->size == 0;
}
此函数用于检查堆中是否有数据,通过查看堆结构体中的 `size` 成员变量是否为0来判断。当堆中没有元素时,函数返回 true,表示堆为空;否则返回 false,表示堆中有至少一个元素。在调用此函数前,需确保堆结构指针有效。

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

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

相关文章

【ZYNQ】Zynq 芯片介绍

Zynq 是 Xilinx 公司提出的全可编程 SoC 架构&#xff0c;集成了单核或多核 ARM 处理器与 Xilinx 16nm 或 28nm 可编程逻辑&#xff0c;包括 Zynq 7000 Soc&#xff0c;Zynq UltraScale MPSoC 和 Zync UltraScale RFSoC 等系列。本文主要介绍 Xilinx Zynq 7000 系列芯片架构、功…

[阅读笔记20][BTX]Branch-Train-MiX: Mixing Expert LLMs into a Mixture-of-Experts LLM

这篇论文是meta在24年3月发表的&#xff0c;它提出的BTX结构融合了BTM和MoE的优点&#xff0c;既能保证各专家模型训练时的高度并行&#xff0c;又是一个统一的单个模型&#xff0c;可以进一步微调。 这篇论文研究了以高效方法训练LLM使其获得各领域专家的能力&#xff0c;例如…

欧科云链:香港虚拟资产OTC合规在即,技术监管成市场规范关键

4月12日香港OTC发牌制度公众咨询结束后&#xff0c;欧科云链研究院在星岛日报发表专栏文章&#xff0c;分享对香港OTC市场的调研情况&#xff0c;并提出“技术监管是香港OTC及Web3生态走向规范的关键”。欧科云链研究院认为&#xff0c;随着OTC监管及虚拟资产现货ETF等事件向前…

【Yolov系列】Yolov5学习(一)补充1.1:自适应锚框计算

1、Yolov5的网络结构 Yolov5中使用的Coco数据集输入图片的尺寸为640*640&#xff0c;但是训练过程的输入尺寸并不唯一&#xff0c;Yolov5可以采用Mosaic增强技术把4张图片的部分组成了一张尺寸一定的输入图片。如果需要使用预训练权重&#xff0c;最好将输入图片尺寸调整到与作…

【剪映专业版】13快速为视频配好音:清晰、无噪声、对齐

视频课程&#xff1a;B站有知公开课【剪映电脑版教程】 使用场景&#xff1a;视频无声音或者视频有声音但是需要更改声音 时间指示器在哪里&#xff0c;就从哪里开始 红色按钮&#xff1a;开始录音 声音波纹&#xff1a;蓝色最佳&#xff0c;黄色或红色声音太大&#xff0c;…

梯度消失/梯度爆炸

梯度消失/梯度爆炸&#xff08;Vanishing / Exploding gradients&#xff09; 梯度消失或梯度爆炸&#xff1a;训练神经网络的时候&#xff0c;导数或坡度有时会变得非常大&#xff0c;或者非常小&#xff0c;甚至于以指数方式变小&#xff0c;这加大了训练的难度。 g ( z ) …

目标检测YOLO数据集的三种格式及转换

目标检测YOLO数据集的三种格式 在目标检测领域&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;算法是一个流行的选择。为了训练和测试YOLO模型&#xff0c;需要将数据集格式化为YOLO可以识别的格式。以下是三种常见的YOLO数据集格式及其特点和转换方法。 1. YOL…

node的事件循环

异步同步啥的就不多说了&#xff0c;直接看node中有哪些是异步 其中灰色部分和操作系统有很大的关系&#xff0c;就不多说了&#xff0c;其中定时器属于timers队列&#xff0c;I/O操作属于poll队列&#xff0c;setImmediate属于check队列&#xff0c;其中nextTick和promise不属…

PTA L2-052 吉利矩阵

题目 解析 这题考的是搜索剪枝 可行性剪枝&#xff1a; 即判断当前行&#xff08;列&#xff09;是否已经超过L和剩下的格子都填最大值是否小于L&#xff0c;若是则剪枝。 当前行数大于1时&#xff0c;判断上一个填完的行是否等于L&#xff0c;若否&#xff0c;则剪枝。 当前行…

【深度学习实战(12)】训练之模型参数初始化

在深度学习模型的训练中&#xff0c;权重的初始值极为重要。一个好的初始值&#xff0c;会使模型收敛速度提高&#xff0c;使模型准确率更精确。一般情况下&#xff0c;我们不使用全0初始值训练网络。为了利于训练和减少收敛时间&#xff0c;我们需要对模型进行合理的初始化。 …

linux 下的 sqlite数据库

SQLite 认识 SQLite简介 轻量化&#xff0c;易用的嵌入式数据库&#xff0c;用于设备端的数据管理&#xff0c;可以理解成单点的数据库。传统服务器型数据库用于管理多端设备&#xff0c;更加复杂 SQLite是一个无服务器的数据库&#xff0c;是自包含的。这也称为嵌入式数据库&…