热门标签 | 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;如有错误请批评指正




推荐阅读
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文介绍了在Vue项目中如何结合Element UI解决连续上传多张图片及图片编辑的问题。作者强调了在编码前要明确需求和所需要的结果,并详细描述了自己的代码实现过程。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 设计模式——模板方法模式的应用和优缺点
    本文介绍了设计模式中的模板方法模式,包括其定义、应用、优点、缺点和使用场景。模板方法模式是一种基于继承的代码复用技术,通过将复杂流程的实现步骤封装在基本方法中,并在抽象父类中定义模板方法的执行次序,子类可以覆盖某些步骤,实现相同的算法框架的不同功能。该模式在软件开发中具有广泛的应用价值。 ... [详细]
  • 文章目录题目:二叉搜索树中的两个节点被错误地交换。基本思想1:中序遍历题目:二叉搜索树中的两个节点被错误地交换。请在不改变其结构的情况下 ... [详细]
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社区 版权所有