初级数据结构(六)——堆

     文中代码源文件已上传:数据结构源码

<-上一篇 初级数据结构(五)——树和二叉树的概念        |        NULL 下一篇->

1、堆的特性

1.1、定义

        堆结构属于完全二叉树的范畴,除了满足完全二叉树的限制之外,还满足所有父节点数据的值均大于(或小于)子节点。

        父节点大于子节点的堆称为大堆或大根堆,反之则称为小堆或小根堆。

        下面例子由于红色节点不符合堆的定义,所以不是堆。

1.2、实现方式

        由于堆每插入一个数据,它的位置是确定的,所以一般都是以顺序表构建堆,插入新节点只相当于顺序表的尾插。这个顺序表与本系列第一篇里定义的顺序表可以说完全一样,区别只在于对表的操作上。当然你也可以用二叉节点或者三叉节点来创建堆,但这样一来后续对堆的操作会特别繁琐。

        在这里,需要重点理解,以顺序表来创建堆其实际结构是线性的,但我们通过对下标序号附以一定意义,把它抽象成树结构。

        访问顺序我们可以先回顾上一篇里的两张图:

        顺序表第一个元素下标为 0 ,我们以它作为根节点,子节点在顺序表中的下标分别是父节点下标的二倍 +1 和二倍 +2 。

size_t child_1 = parent * 2 + 1;
size_t child_2 = parent * 2 + 2;
size_t parent = (child_1 - 1) / 2;
size_t parent = (child_2 - 1) / 2;

        若取下标为 1 的位置作为根节点的情况下( 0 的位置空置即可),子节点在顺序表中的下标分别是父节点下标的二倍和二倍 +1 。

size_t child_1 = parent * 2;
size_t child_2 = parent * 2 + 1;
size_t parent = child_1 / 2;
size_t parent = child_2 / 2;

2、堆构建

2.1、文件结构

        以顺序表的方式构建堆,这次选用柔性数组的结构体形式。与之前相同的三个文件:

        heap.h :用于创建项目的结构体类型以及声明函数;

        heap.c :用于创建堆各种操作功能的函数;

        main.c :仅创建 main 函数,用作测试。

2.2、前期工作

        heap.h 中内容如下。这里需要注意的是,由于堆是以 malloc 形式创建的空间,以指针记录,销毁堆的函数最终需要把该指针变量置空,所以需要传指针的地址。而插入数据和删除数据由于涉及 realloc ,有异地扩容的可能,同样需要改变堆指针记录的地址,所以这三个函数参数都必须定义为二级指针:

#include <stdio.h>
#include <stdlib.h>//大堆大于号 小堆小于号
#define COMPARE <//存储数据类型的定义及打印占位符预定义
#define DATAPRT "%d"
typedef int DATATYPE;//堆结构体类型
typedef struct Heap
{size_t size;		//记录堆内数据个数size_t capacity;	//记录已开辟空间大小DATATYPE data[0];	//数据段
}Heap;//函数声明-----------------------------------
//创建堆
extern Heap* HeapCreate();
//销毁堆
extern void HeapDestroy(Heap**);
//插入数据
extern void HeapPush(Heap**, DATATYPE);
//删除数据
extern void HeapPop(Heap**);

        然后是 heap.c :

#include "heap.h"//创建堆
Heap* HeapCreate()
{//创建堆空间Heap* heap = (Heap*)malloc(sizeof(Heap) + sizeof(DATATYPE) * 4);//创建结果检查if (!heap){fprintf(stderr, "Malloc Fail\n");return NULL;}//初始化储存记录heap->size = 0;heap->capacity = 4;return heap;
}//销毁堆
void HeapDestroy(Heap** heap)
{//堆地址有效性检查if (!heap || !*heap){fprintf(stderr, "Heap Address NULL\n");return;}//销毁堆空间free(*heap);*heap = NULL;
}

        这次就不每一步都测试了,构建过程中可以自行测试。所以只需要在 main.c 中写入 include 头文件和 main 函数的壳即可:

#include "heap.h"int main()
{return 0;
}

3、堆的数据操作

        由于堆的特性,主要只涉及增加数据及删除数据两个功能查找和修改在堆的操作上没有意义。此外,本节的全部代码均写在 heap.c 之中。

3.1、插入数据

        插入数据实际上是对顺序表的尾插,但是尾插之后的堆很可能不符合堆的定义,因此,尾插之后还需对堆进行调整。调整步骤是不断地将插入的数据与父节点进行比较,如果不符合大堆或者小堆的规律,则互换。

        这种操作称作向上调整,也叫做上滤。以下是上滤操作的代码,由于只在 heap.c 中调用,用 static 修饰比较好。

//上滤
static void HeapFilterUp(Heap* heap)
{//堆地址有效性检查if (!heap){fprintf(stderr, "Heap Address NULL\n");return NULL;}//获取初始父节点子节点下标size_t child = heap->size - 1;size_t parent = (child - 1) / 2;while (child != 0){//如果不满足堆的条件if (heap->data[child] COMPARE heap->data[parent]){//向上交换数据DATATYPE tempData = heap->data[child];heap->data[child] = heap->data[parent];heap->data[parent] = tempData;//计算新的父子节点下标child = parent;parent = (parent - 1) / 2;}else{break;}}
}

        上滤函数中有两个比较容易坑的点,首先是循环条件应该是子节的位置作为判断依据,当子节点下标为 0 时说明已经到根节点了,至此中断循环。此外,当交换到某个位置时已经满足堆的特性,记得中断循环。

        完成上滤函数之后就可以开始写插入数据的函数主体了:

//插入数据
void HeapPush(Heap** ptr_heap, DATATYPE data)
{//堆地址有效性检查if (!ptr_heap || !*ptr_heap){fprintf(stderr, "Heap Address NULL\n");return;}//空间不足则扩容if ((*ptr_heap)->size >= (*ptr_heap)->capacity){Heap* tempHeap = NULL;while (!tempHeap){tempHeap = (Heap*)realloc(*ptr_heap, sizeof(Heap) + sizeof(DATATYPE) * (*ptr_heap)->capacity * 2);}*ptr_heap = tempHeap;(*ptr_heap)->capacity *= 2;}//数据插入堆尾(*ptr_heap)->data[(*ptr_heap)->size] = data;(*ptr_heap)->size++;//上滤HeapFilterUp(*ptr_heap);
}

3.2、删除数据

        这部分有点像由顺序表构建的队列( FIFO 属性)。堆删除数据总是删除根节点。但是删除根节点后,并不能如队列般将后面的元素往前挪,原因如下图:

        因为堆的顺序与队列的顺序不一样,既然是堆,则不能以队列的方式挪动数据。

        堆删除数据的常规的方式是将最后一个节点覆盖到根节点,然后将 size - 1 。之后与上滤类似,堆挪动数据的方式称为下滤或向下调整。过程是:先比较两个子节点的大小,如果是大堆,则取较大的子节点,再以较大的子节点与父节点比较,如果不符合堆的特性,则两者互换,一直到叶节点。具体看下图例子。

         根据这个思路,先凹一个下滤函数:

//下滤
static void HeapFilterDown(Heap* heap)
{//堆地址有效性检查if (!heap){fprintf(stderr, "Heap Address NULL\n");return NULL;}//获取初始父节点子节点下标size_t parent = 0;size_t child = 1;while (child < heap->size){//将左右两个子节点中数据较大值的节点下标赋予childif (child + 1 < heap->size && heap->data[child + 1] COMPARE heap->data[child]){child++;}//如果不满足堆的条件if (heap->data[child] COMPARE heap->data[parent]){//向下交换数据DATATYPE tempData = heap->data[child];heap->data[child] = heap->data[parent];heap->data[parent] = tempData;//计算新的父子节点下标parent = child;child = child * 2 + 1;}else{break;}}
}

        刚才写完上滤函数之后,写下滤函数最容易入一个坑就是循环条件以父节点下标是否超过数据个数作判定,但当父节点为叶节点时,子节点下标便已经超过数据个数了。当然也可以以父节点是否有子节点判定,但本质上,这还是判定子节点。

        此外还有一个坑,时刻需要注意如果父元素存在左子节点,不一定存在右子节点,因此还需要对右子节点的下标是否超过数据个数作判定。

        下滤函数完成后,删除数据自然信手拈来:

//删除数据
void HeapPop(Heap** ptr_heap)
{//堆地址有效性检查if (!ptr_heap || !*ptr_heap){fprintf(stderr, "Heap Address NULL\n");return;}//空堆直接返回if ((*ptr_heap)->size == 0){fprintf(stderr, "Empty Heap\n");return;}(*ptr_heap)->data[0] = (*ptr_heap)->data[(*ptr_heap)->size - 1];(*ptr_heap)->size--;//空间过剩则回收if ((*ptr_heap)->size < (*ptr_heap)->capacity / 2 && (*ptr_heap)->capacity > 4){Heap* tempHeap = NULL;while (!tempHeap){tempHeap = (Heap*)realloc(*ptr_heap, sizeof(Heap) + sizeof(DATATYPE) * (*ptr_heap)->capacity / 2);}*ptr_heap = tempHeap;(*ptr_heap)->capacity /= 2;}//下滤HeapFilterDown(*ptr_heap);
}

        这里还多加了回收多余空间的语句,这步可以省略。因为堆的使用往往是一次性的,它不是用来长久保存数据的,更像是辅助其他算法的一种临时结构,所以用过之后即销毁,就没必要中途回收空间了。

3.3、其他功能

        这部分功能可有可无这里仅展示代码,当然也可以根据自己需要另外添加其他功能。

//获取堆顶数据
DATATYPE HeapGetData(Heap* heap)
{//堆地址有效性检查if (!heap){fprintf(stderr, "Heap Address NULL\n");return -1;}//空堆直接返回if (heap->size == 0){fprintf(stderr, "Empty Heap\n");return -2;}return heap->data[0];
}//打印堆顶数据
void HeapPrint(Heap* heap)
{//堆地址有效性检查if (!heap){fprintf(stderr, "Heap Address NULL\n");return;}//空堆直接返回if (heap->size == 0){fprintf(stderr, "Empty Heap\n");return;}printf(DATAPRT" ", heap->data[0]);
}//打印堆
void HeapPrintAll(Heap* heap)
{//堆地址有效性检查if (!heap){fprintf(stderr, "Heap Address NULL\n");return;}//空堆直接返回if (heap->size == 0){fprintf(stderr, "Empty Heap\n");return;}int enterSite = 0;for (int i = 0; i < heap->size; i++){printf(DATAPRT" ", heap->data[i]);if (enterSite == i){printf("\n");enterSite = enterSite * 2 + 2;}}
}

        最后别忘了在 heap.h 中加入声明:

//获取堆顶数据
extern DATATYPE HeapGetData(Heap*);
//打印堆顶数据
extern void HeapPrint(Heap*);
//打印堆
extern void HeapPrintAll(Heap*);

4、堆排序

4.1、测试

        堆最常见的作用便是堆排序了。因为堆的特性是根节点的数据是整个堆的最大值或者最小值,而且下滤的效率比很多排序方法都高。刚好上面完成的堆结构还没进行测试,所以这里以堆排序作测试。

        main.c 中 main 函数补充如下:

int main()
{//堆排序测试DATATYPE src[30] = { 25,73,60,108,104,336,457,90,668,732,102,1,752,262,776,538,410,442,962,228,873,656,260,18,24,733,520,1414,339,439 };DATATYPE dest[30] = { 0 };//建堆Heap* heap = HeapCreate();//将src中的元素入堆for (int i = 0; i < 30; i++){HeapPush(&heap, src[i]);}//堆排序for (int i = 0; i < 30; i++){dest[i] = HeapGetData(heap);HeapPop(&heap);}//输出排序前后结果printf("\n排序前: ");for (int i = 0; i < 30; i++){printf("%d ", src[i]);}printf("\n排序后: ");for (int i = 0; i < 30; i++){printf("%d ", dest[i]);}//销毁堆HeapDestroy(&heap);return 0;
}

        调试得到结果:

        就此测试完成。

4.2、优化思路

        实际上堆排序上述方式有点拖沓了。由于堆往往用后即毁,所以在进入排序步骤时,不再另外创建数组,二十直接在堆中操作。此时堆的结构虽然被破坏了,但都到这一步了,基本面临销毁,在销毁前加以利用还能节省空间。

        上述思路的堆排序与删除数据仅有一点点区别,在于,排序时,是将根节点与最末尾节点进行互换,而非覆盖。流程如下图:

         因此,只需要把删除数据的函数改改:

//堆排序
void HeapSort(Heap* heap)
{//堆地址有效性检查if (!heap){fprintf(stderr, "Heap Address NULL\n");return;}//空堆直接返回if (heap->size == 0){fprintf(stderr, "Empty Heap\n");return;}//排序while (heap->size){//交换头尾数据DATATYPE temp = heap->data[0];heap->data[0] = heap->data[heap->size - 1];heap->data[heap->size - 1] = temp;heap->size--;//下滤HeapFilterDown(heap);}
}

        别忘了在 heap.h 中声明:

//堆排序
extern void HeapSort(Heap*);

        之后重写 main 函数:

int main()
{//堆排序测试DATATYPE src[30] = { 25,73,60,108,104,336,457,90,668,732,102,1,752,262,776,538,410,442,962,228,873,656,260,18,24,733,520,1414,339,439 };//建堆Heap* heap = HeapCreate();//将src中的元素入堆for (int i = 0; i < 30; i++){HeapPush(&heap, src[i]);}//堆排序HeapSort(heap);//重新指定下size,不然打印不出来heap->size = 30;//打印HeapPrintAll(heap);//销毁堆HeapDestroy(&heap);return 0;
}

        F5 走起:

        结果正确。完事!

4.3、衍生 TopK 算法

        堆除了排序之外,还可用于解决 TopK 问题。首先,什么是 TopK ?

        一句话解释, TopK 就是取数据列表中最大或者最小的前 K 个数据。回想堆排序的过程,HeapSort 函数中的 while 循环 n 次是排序 n+1 个数据列表中的最值,那么,是否可以理解成堆排序实际上就是 K 等于数据个数 -1 的 TopK 算法?

        换个方式说,堆排序是对所有节点进行排序,而 TopK 只需排序前 K 个节点即可,也就是说,假设数据个数是 n ,堆排序是对堆进行 n-1 次首尾互换后下滤操作,而 TopK 则是执行 K-1 次首位互换后下滤的操作。其中,K ≤ n 。

        改改 HeapSort 函数就行了,所以这里补贴代码了,各位可以自行尝试。本篇至此结束。

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

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

相关文章

SolidWorks二次开发 C#-读取基于Excel的BOM表信息

SolidWorks二次开发 C#-读取基于Excel的BOM表信息 问题点来源解决方案及思路相关引用链接 问题点来源 这是一位粉丝问的一个问题&#xff0c;他说到: 老师&#xff0c;请问Solidworks二次开发工程图中"基于Excel的材料明细表"怎么读取里面的数据&#xff1f; Ps:这…

leetcode刷题日志-383赎金信

思路&#xff1a;分别用两个map记录ransomNote和magazine中的字符以及出现的次数。最后遍历记录ransomNote的map&#xff0c;如果ransomNote的map中出现的magazine的map中没有出现或者出现的次数小于ransomNote的map则返回false&#xff0c;否则返回true&#xff1b; class So…

arcgis api for js 图层标注文本不显示

在打包arcgis api for js 项目后&#xff0c;更新到现场所有地图文本要素不显示的时候。 可能就是环境设置问题了&#xff0c;需要配置iis或者ngixs里边配置如下类型 iis发布的项目 1、打开iis中发布的网站 2、找到网站的MIME类型如下图 3、添加一下类型&#xff08;一个都不…

机器学习项目精选 第一期:超完整数据科学资料合集

大噶吼&#xff0c;不说废话&#xff0c;分享一波我最近看过并觉得非常硬核的资源&#xff0c;包括Python、机器学习、深度学习、大模型等等。 1、超完整数据科学资料合集 地址&#xff1a;https://github.com/krishnaik06/The-Grand-Complete-Data-Science-Materials Pytho…

【Linux API 揭秘】container_of函数详解

我的圈子&#xff1a; 高级工程师聚集地 我是董哥&#xff0c;高级嵌入式软件开发工程师&#xff0c;从事嵌入式Linux驱动开发和系统开发&#xff0c;曾就职于世界500强企业&#xff01; 创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01; …

1852_bash中的find应用扩展

Grey 全部学习内容汇总&#xff1a; https://github.com/GreyZhang/toolbox 1852_bash中的find应用扩展 find这个工具我用了好多年了&#xff0c;但是是不是真的会用呢&#xff1f;其实不然&#xff0c;否则也不会出现这种总结式的笔记。其实&#xff0c;注意部分小细节之后…

liunx之Samba服务器

环境&#xff1a;虚拟机CENTOS 7和 测试机相通 一、Samba服务器_光盘共享&#xff08;匿名访问&#xff09; 1.在虚拟机CENTOS 7安装smb服务&#xff0c;并在防火墙上允许samba流量通过 2. 挂载光盘 3.修改smb.conf配置文件&#xff0c;实现光盘匿名共享 4. 启动smb服务 5.在…

手麻、腿麻、麻痛…背后竟隐藏7大疾病!多一个人知道,少一个悲剧!

手脚麻木背后的7大病症&#xff1a;骨病、脑梗、肿瘤…… 1、神经问题 上图四只手上橙色的区域代表了麻木感&#xff0c;如果您的手麻集中在无名指和小指的区域&#xff0c;您可以拿一张纸&#xff0c;用五个手指分别试着夹住&#xff0c;检验您的五个手指力量&#xff1b;您还…

(5)shell命令以及Linux的权限

写在前面 本章我们将重点讲解 Linux 权限&#xff0c;这是 Linux 基础部分中非常重要的一部分。内容比较干&#xff0c;我会稍稍正经些去讲解。话不多说&#xff0c;我们直接切入正题。 shell 命令及运行原理 严格意义上说的是一个操作系统&#xff0c;我们称之为 —— &…

Logstash访问安全访问Elasticsearch集群

生成logstash证书: opensal pkcs12 -in elastic-stack-ca.p12 -clcerts -nokeys > logafash.cer openssl x509 -in logstash.cer -out logstash.pem 编排配置文件

xtu oj 1328 数码和

题目描述 一个10进制数n在2∼16进制下可以得到的不同的数码和&#xff0c;求在这些数码和中出现次数最多的数码和。 比如20&#xff0c; 其中数码和2和4分别出现了3次&#xff0c;为最多出现次数。 输入 第一行是一个整数T(1≤T≤1000)&#xff0c;表示样例的个数。 以后每行…

Sherlock:社交媒体账号搜索工具 | 开源日报 No.111

sherlock-project/sherlock Stars: 45.1k License: MIT 这个项目是一个社交媒体账号搜索工具&#xff0c;名为 Sherlock。它可以通过用户名在不同的社交网络上追踪用户的账号。 支持单个或多个用户搜索提供 Docker 容器化部署方式开放源代码并欢迎贡献者参与开发包含详细测试…

Shell三剑客:sed(命令)一

一、删除命令&#xff1a;d [rootlocalhost ~]# sed -r /root/d passwd.txt bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync…

Vim入门

Vim使用入门 1.Vim编辑器的三种常用模式 一般模式&#xff1a;刚打开文件是它&#xff0c;从编辑模式按“ESC”退回的模式也是它。可以执行各种编辑操作&#xff0c;如移动光标、复制、粘贴、删除、查找替换等 ; 编辑模式&#xff1a;在一般模式下按下 i、I、a、A、o、O 等键…

CesiumLab地理信息基础数据处理平台 各类数据类型介绍、发布数据介绍

目录 0 引言1 CesiumLab2 数据处理模块2.1 输出格式&#xff1a;切片文件格式2.2 输入格式2.2.1 传统GIS数据2.2.2 人工模型2.2.3 BIM模型2.2.4 倾斜实景数据2.2.5 点云数据 3 发布服务功能3.1 拓展&#xff1a;其他平台发布服务功能 &#x1f64b;‍♂️ 作者&#xff1a;海码…

再怎么“顽固”的应用程序,也很难经得住以下的卸载方法

卸载程序是我们经常尝试的事情。这可能是因为我们不再需要程序,该程序可能会导致问题等。有时,如果你试图卸载某个程序,你会收到一个错误,但卸载没有发生。在这种情况下,你可以选择强制卸载。在本教程中,我将向你展示如何在Windows 10/11计算机上强制卸载程序。 ​控制面…

Mybatis的插件运⾏原理,如何编写⼀个插件?

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

2024年最免费的DAW混音编曲FL Studio21.2.中文版下载

FL Studio 简称FL&#xff0c;全称Fruity Loops Studio&#xff0c;因此国人习惯叫它"水果"。目前最新版本是FL Studio21.1.1.3750版本&#xff0c;它让你的计算机就像是全功能的录音室&#xff0c;大混音盘&#xff0c;非常先进的制作工具&#xff0c;让你的音乐突破…

AUTOSAR组织引入了Rust语言的原因是什么?有哪些好处?与C++相比它有什么优点?并推荐一些入门学习Rust语言链接等

AUTOSAR(汽车开放系统架构)是一个由汽车制造商、供应商和其他来自电子、半导体和软件行业的公司组成的全球发展伙伴关系,自2003年以来一直致力于为汽车行业开发和引入开放、标准化的软件平台。 AUTOSAR 最近宣布成立一个新的工作组,用于探索在汽车软件中使用 Rust 编程语言…

C语言实现在顺序表中找到最大值

用C语言实现在顺序表中找到最大值&#xff1a; #include <stdio.h> #define MAX_SIZE 100 int findMax(int arr[], int size) { int max arr[0]; // 假设第一个元素为最大值 for (int i 1; i < size; i) { // 从第二个元素开始遍历列表 if (…