热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:数据结构:堆

篇首语:本文由编程笔记#小编为大家整理,主要介绍了数据结构:堆相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了数据结构:堆相关的知识,希望对你有一定的参考价值。








文章目录


  • 前言
  • 一、堆
  • 二、顺序存储
  • 三、堆的实现
    • 1.建堆
    • 2.向堆中插入数据
    • 3.删除堆顶的数据
    • 4.其他对堆的操作

  • 感谢阅读,如有错误请批评指正





前言

在数据结构(四):二叉树中,树是通过链式结构来实现的。在本文中,堆将通过顺序结构实现。同样是树,为什么实现时存储方式不同呢?堆又有哪些特殊的性质呢?

需要注意,本文介绍的堆和操作系统虚拟进程地址空间中的不同,前者是一种数据结构,后者是操作系统中管理内存的一块区域分段。




一、堆

如果有一个数据的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中。且满足以下性质:

(1)每个节点的值总是不大于或不小于其父节点的值;
(2)是一棵完全二叉树。

那么这就是一个堆。

如果根节点的值是堆中最小的值,那么这是一个小根堆(大堆)。
如果根节点的值是堆中最大的值,那么这是一个大根堆(小堆)。

在这里插入图片描述




二、顺序存储

顺序结构存储就是使用数组来存储,一般只适合表示完全二叉树,因为如果不是完全二叉树,存储时会有空间的浪费。由于堆实际上是一棵完全二叉树,所以堆可以使用数组来存储。

顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
在这里插入图片描述



(来自百度图片)

由图中可以看出,左侧一棵完全二叉树存储进一个数组时不存在空间浪费;而右侧非完全二叉树在存储时下标为3、6、7、8的位置没有数据,造成了空间的浪费。




三、堆的实现

堆的结构如下:

typedef int HPData;
typedef struct Heap
{
HPData* a;
int size;//当前堆中数据个数
int capacity;//最大容量
}HP;

可以看到,堆的物理结构实际上是一个可动态增长的数组。


1.建堆

下面给出一个数组,逻辑上看作一个堆,现在把它调整成大堆。

int a[] = { 15, 18, 28, 34, 65, 19, 49, 25, 37, 27 };

在这里插入图片描述
调整的思路:从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点,就可以把整棵树调整成堆。

下面以上图为例进行调整:

(1)倒数第一个非叶子结点:65,左右子树的值都比它小(忽略空),不需要调整。



(2)倒数第二个非叶子结点:34,左子树的值比它小,右子树的值比它大,交换它与右子树的值。(新的堆如下)
在这里插入图片描述
以37为根节点的堆是大堆,继续找前一个非叶子节点。



(3)倒数第三个非叶子结点:28,左子树的值比它小,右子树的值比它大,交换它与右子树的值。(新的堆如下)
在这里插入图片描述
以49为根节点的堆是大堆,继续找前一个非叶子节点。



(4)倒数第四个非叶子结点:18,左右子树的值都比它大,这时选取左右子树中大的那个值与它交换,即交换它与右子树的值。(新的树如下,注意这是不是堆)
在这里插入图片描述
这时以18为根节点的堆显然不是大堆,所以需对这个结点再调整,调整逻辑同上。(新的堆如下)
在这里插入图片描述



(5)倒数第五个非叶子结点:15(访问到根结点,调整完根结点后结束),左子右树的值都比它大,这时选取左右子树中大的那个值与它交换,即交换它与左子树的值。(新的堆如下)
在这里插入图片描述
此时以15位根的子树不是大堆,按照上述逻辑继续调整。(最终的堆如下)

在这里插入图片描述

已经访问到根结点并将根结点调整完毕,调整结束。此时这个堆已经是一个大堆。



上面的调整方法叫做向下调整算法。

但是向下调整算法有一个前提:左右子树必须已经是一个堆,才能调整。

所以必须从倒数第一个非叶子结点开始调整,当调整到前面的非叶子结点时,由于它后面的非叶子结点都已经是大堆,所以这个结点的左右子树也都是大堆,就可以继续使用向下调整算法了。

代码中还用到完全二叉树的一个规律 (根结点是0时成立) :如果一个结点的下标为n,那么它的左孩子的下标为(2 * n +1),右孩子的下标为(2 * n +2),父结点的下标(n - 1)/ 2。



下面是代码:

代码如下(示例):

//交换a、b的值
void Swap(HPData* a, HPData* b)
{
HPData temp = *a;
*a = *b;
*b = temp;
}
//建大堆
void AdjustDown(HPData* a, int n, int parent)
{
int child = parent * 2 + 1;//找到左孩子的下标
while (child < n)//调整某一个结点的所有子树&#xff0c;直到符合要求
{
//这里注意要先判断child&#43;1是否越界
if (child &#43; 1 < n && a[child &#43; 1] > a[child])//如果右孩子的值大于左孩子&#xff0c;child指向右孩子的下标
child&#43;&#43;;
//如果child结点的值比parent结点的值大&#xff0c;就交换数值&#xff0c;并更新child和parent结点继续向下调整
if (a[child] > a[parent])
{
Swap(&a[parent], &a[child]);
parent &#61; child;
child &#61; parent * 2 &#43; 1;
}
//如果child结点的值比parent结点的值小&#xff0c;说明已经是大堆&#xff0c;循环结束
else
break;
}
}
//将一个数组建堆&#xff0c;n为数组的大小
void HeapInit(HP* php, HPData* a, int n)
{
assert(php);
php->a &#61; (data*)malloc(sizeof(data)*n);
if (php->a &#61;&#61; NULL)
{
printf("malloc fail\\n");
exit(-1);
}
php->size &#61; n;
php->capacity &#61; n;
memcpy(php->a, a, sizeof(data)*n);//把数组a中的内容拷贝到php的动态增长的数组中
int i &#61; 0;
//从倒数第一个非叶子结点开始调整
//php->size - 1是最后一个结点的下标&#xff1b;(php->size - 1 - 1) / 2是这个结点的父结点也就是倒数第一个非叶子结点
for (i &#61; (php->size - 1 - 1) / 2; i >&#61; 0; i--)
AdjustDown(php->a, php->size, i);//从最后一个非叶子节点开始依次向上调整
}



2.向堆中插入数据

向堆中插入数据时&#xff0c;不能简单的在php->a的末尾加上一个数据&#xff0c;必须要保证插入这个数据后的树仍是一个堆。

这里要用到向上调整算法。就是找到插入数据后最后一个数据的父结点&#xff0c;判断是否符合大堆的特征&#xff0c;如果不符合&#xff0c;交换。重复这一过程直到访问到根结点或者父结点的值大于孩子结点。

以上面的向下调整算法建好的堆为例演示插入数据&#xff1a;
在这里插入图片描述



&#xff08;1&#xff09;插入的数据40的父结点是27&#xff0c;40大于27&#xff0c;所以以27为根结点的子树不是大堆&#xff0c;交换27和40。&#xff08;结果如下&#xff09;
在这里插入图片描述



&#xff08;2&#xff09;继续向上调整&#xff0c;40的父结点是37&#xff0c;40大于37&#xff0c;所以以37为根结点的子树不是大堆&#xff0c;交换37和40。&#xff08;结果如下&#xff09;
在这里插入图片描述



&#xff08;3&#xff09;继续向上调整&#xff0c;发现此时已经访问到根结点&#xff0c;而且根所在的树已经是大堆&#xff0c;调整完毕。



代码如下&#xff1a;

代码如下&#xff08;示例&#xff09;&#xff1a;

//向上调整
void AdjustUp(int* a, int child)
{
if (child <&#61; 0)
return;
int parent &#61; (child - 1) / 2;//找到父结点
if (parent >&#61; 0 && a[parent] < a[child])//父结点不越界且不符合大堆就交换
{
Swap(&a[parent], &a[child]);
child &#61; parent;
AdjustUp(a, child);//更新child&#xff0c;递归调整
}
else
return;
}
//向堆中插入数据
void HeapPush(HP* php, HPData x)
{
assert(php);
if (php->size &#61;&#61; php->capacity)//数据满就扩容
{
HPData* tmp &#61; (HPData*)realloc(php->a, php->capacity * 2 * sizeof(HPData));
if (tmp &#61;&#61; NULL)
{
printf("realloc fail\\n");
exit(-1);
}
php->a &#61; tmp;
php->capacity *&#61; 2;
}
//把x插入到php->a的末尾
php->a[php->size] &#61; x;
php->size&#43;&#43;;
//向上调整变成堆
AdjustUp(php->a, php->size - 1);
}



3.删除堆顶的数据

最简单的思路是把php->a中的数据从第二个开始整体前移一位&#xff0c;然后再向下调整&#xff0c;但是这样实现起来挪动数据需要时间&#xff0c;由于堆被打乱重新调堆又需要时间&#xff0c;时间复杂度太高。

这里采用一种巧妙的思路&#xff1a;交换堆中第一个和最后一个数据&#xff0c;再让php->size减一&#xff0c;这就相当于删除了第一个数据&#xff08;因为size减一后已经访问不到了&#xff09;&#xff0c;之后再对堆进行向下调整即可。这种思路不需要挪动数据&#xff0c;同时整个树中只有根节点可能不是堆&#xff0c;剩下的子树都仍是堆。时间复杂度很低。

//删除堆顶数据&#xff08;最大的数据&#xff09;
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);
Swap(&php->a[0], &php->a[php->size - 1]);//交换堆中第一个和最后一个数据
php->size--;
AdjustDown(php->a, php->size, 0);//对根进行向下调整
}



4.其他对堆的操作

下面的函数实现时较简单&#xff0c;此处不再赘述。

代码如下&#xff08;示例&#xff09;&#xff1a;

//得到堆顶数据
HPData HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);
return php->a[0];
}
//得到堆中数据的个数
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
assert(php);
return (php->size &#61;&#61; 0);
}
//堆的销毁
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);

php->a &#61; NULL;
php->capacity &#61; 0;
php->size &#61; 0;
free(php);
}



感谢阅读&#xff0c;如有错误请批评指正




推荐阅读
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • 深入解析 Synchronized 锁的升级机制及其在并发编程中的应用
    深入解析 Synchronized 锁的升级机制及其在并发编程中的应用 ... [详细]
  • 作文记录:合并区间的技巧与应用
    本文详细记录了合并区间问题的解题技巧与应用场景。首先介绍了问题背景和题目描述,接着从排序最大值的角度探讨了解决思路,并提供了具体的程序代码及运行结果。此外,还探讨了其他可能的解决方案。最后,对整个解题过程进行了总结,为读者提供了全面的理解和参考。 ... [详细]
  • 泰波那契数列与斐波那契数列类似,但其计算方法有所不同。本文详细解析了如何高效计算第 N 个泰波那契数,并提供了一种基于动态规划的优化算法。通过使用数组记录中间结果,避免了重复计算,显著提高了算法的执行效率。代码示例展示了具体的实现方法,帮助读者更好地理解和应用这一算法。 ... [详细]
  • Halcon之图像梯度、图像边缘、USM锐化
    图像梯度、图像边缘、USM锐化图像梯度、图像边缘、USM锐化图像梯度、图像边缘、USM锐化图像卷积:1.模糊2.梯度3.边缘4.锐化1.视频教程:B站、 ... [详细]
  • 文章目录Golang定时器Timer和Tickertime.Timertime.NewTimer()实例time.AfterFunctime.Tickertime.NewTicke ... [详细]
  • 本文详细介绍了MySQL数据库的基础语法与核心操作,涵盖从基础概念到具体应用的多个方面。首先,文章从基础知识入手,逐步深入到创建和修改数据表的操作。接着,详细讲解了如何进行数据的插入、更新与删除。在查询部分,不仅介绍了DISTINCT和LIMIT的使用方法,还探讨了排序、过滤和通配符的应用。此外,文章还涵盖了计算字段以及多种函数的使用,包括文本处理、日期和时间处理及数值处理等。通过这些内容,读者可以全面掌握MySQL数据库的核心操作技巧。 ... [详细]
  • 2.2 组件间父子通信机制详解
    2.2 组件间父子通信机制详解 ... [详细]
  • 本文提出了一种基于栈结构的高效四则运算表达式求值方法。该方法能够处理包含加、减、乘、除运算符以及十进制整数和小括号的算术表达式。通过定义和实现栈的基本操作,如入栈、出栈和判空等,算法能够准确地解析并计算输入的表达式,最终输出其计算结果。此方法不仅提高了计算效率,还增强了对复杂表达式的处理能力。 ... [详细]
  • 深入解析:Synchronized 关键字在 Java 中对 int 和 Integer 对象的作用与影响
    深入探讨了 `Synchronized` 关键字在 Java 中对 `int` 和 `Integer` 对象的影响。尽管初看此题似乎简单,但其实质在于理解对象的概念。根据《Java编程思想》第二章的观点,一切皆为对象。本文详细分析了 `Synchronized` 关键字在不同数据类型上的作用机制,特别是对基本数据类型 `int` 和包装类 `Integer` 的区别处理,帮助读者深入理解 Java 中的同步机制及其在多线程环境中的应用。 ... [详细]
  • 你的问题在于:1. 代码格式混乱,缺乏必要的缩进,导致可读性极低;2. 使用 `strlen()` 和 `malloc()` 函数时,必须包含相应的头文件;3. `write()` 函数的返回值处理不当,建议检查并处理其返回值以确保程序的健壮性。此外,建议在编写代码时遵循良好的编程规范,增加代码的可维护性和可读性。 ... [详细]
  • 在2022年11月2日的AcWing每日编程挑战中,任务是计算一个长度为n的整数序列中的逆序对数量。逆序对是指在序列中,若存在两个下标i和j(i < j),且a[i] > a[j],则称这两个元素构成一个逆序对。本题要求实现一个算法来高效地统计这些逆序对的数量。 ... [详细]
  • 本文深入解析了JDK 8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。 ... [详细]
  • 本文探讨了如何通过编程手段在Linux系统中禁用硬件预取功能。基于Intel® Core™微架构的应用性能优化需求,文章详细介绍了相关配置方法和代码实现,旨在帮助开发人员有效控制硬件预取行为,提升应用程序的运行效率。 ... [详细]
author-avatar
京江晚报经济民生部
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有