数据结构·二叉树(2)

目录

1 堆的概念

2 堆的实现

2.1 堆的初始化和销毁

2.2 获取堆顶数据和堆的判空

2.3 堆的向上调整算法

2.4 堆的向下调整算法

2.4 堆的插入

2.5 删除堆顶数据

2.6 建堆

3 建堆的时间复杂度

3.1 向上建堆的时间复杂度

3.2向下建堆的时间复杂度

4 堆的排序


前言:前面介绍了树以及二叉树及其二叉树的存储方式,本文就介绍基于二叉树模式下的一种结构——堆。


1 堆的概念

堆分为大堆和小堆,小堆是指每个父节点的值都小于子节点,大堆是子节点的值都大于父节点,

小堆是这样的,大堆同理就可以了。

堆在逻辑上是完全二叉树结构,实际的物理结构是数组,接下来就进入到重点——堆的实现。


2 堆的实现

实现堆的时候,我们不像之前实现顺序表的时候,有增删查改以及指定位置的删除增加等等,因为堆单纯用来存储数据是没有太大的意义的,所以实现的接口也不大一样。

堆同样用结构体定义,一个是数据,一个是空间大小,一个是有效数据个数。


typedef int HDataType;typedef struct Heap
{HDataType* arr;int size;int capacity;
}Heap;//堆的初始化
void HPInit(Heap* php);//建堆
void HPInitArray(Heap* php,HDataType* a, int n);//堆的销毁
void HPDestroy(Heap* php);//堆的插入
void HPPush(Heap* php,HDataType x);//获取堆顶数据
HDataType HPTop(Heap* hp);//堆的删除数据
void HPPop(Heap* php);//堆的判空
bool HPempty(Heap* php);//堆的向上调整算法
void AdjustUp(HDataType* arr,int child);//堆的向下调整算法
void AdjustDown(HDataType* arr,int size ,int parent);

2.1 堆的初始化和销毁

销毁和初始化与之前线性表的初始化基本上就是一样的,不用过多介绍

void HPInit(Heap* php)
{assert(php);php->arr = NULL;php->capacity = php->size = 0;
}
//堆的销毁
void HPDestroy(Heap* php)
{assert(php);free(php->arr);php->arr = NULL;php->capacity = php->size = 0;
}

2.2 获取堆顶数据和堆的判空

获取数据只需要判断堆是不是空的就行,判空只需要检查size的值就可以了。

bool HPempty(Heap* php)
{assert(php);return php->size == 0;
}
//获取堆顶数据
HDataType HPTop(Heap* php)
{assert(php);assert(!HPempty(php));return php->arr[0];
}

因为后面的向上调整和向下调整,我们对于数据的交换用的是很频繁的,所以我们单独创建一个函数用来交换数据:

//交换数据
void Swap(HDataType* px, HDataType* py)
{HDataType tem = *px;*px = *py;*py = tem;
}

2.3 堆的向上调整算法

堆的向上调整算法,即是我们插入数据之后,保持数据的结构依然是堆,所以向上调整就是从最后一个数据入手,往上依次调整,如果堆是小堆,那么就是子节点与父节点比较大小,子节点小于父节点,就交换,大堆同理可得。

那么向上调整,我们知道子节点,如何求的父节点呢?

其实通过节点之间的存储规律,我们可以得到

左子节点 = 父节点 * 2 + 1,右子节点 = 父节点 * 2 + 2;

知道任意子节点我们就可以求父节点,实际操作的时候我们求父节点的时候怎么知道子节点是左还是右呢?

解决方法就是不管三七二十一,父节点 = (子节点 - 1)  / 2,不管多出来的1,因为整型运算,1 / 2 = 0,所以1是被忽略了。

因为调整的次数可能不止一次,可能调整到高度的一半就停止了,或者是调整到了根部,所以我们使用while循环,循环条件就是子节点的下标,因为经历一次调整后,子节点会到父节点上,父节点又到该节点的父节点上,那么判断条件就应该是子节点的下标位置。

//堆的向上调整算法
void AdjustUp(HDataType* arr, int child)
{int parent = (child - 1) / 2;//注意大小堆的写法while (child){if (arr[child] < arr[parent]){Swap(&arr[child],&arr[parent]);child = parent;parent = (child - 1) / 2;}else{break;}}	
}

2.4 堆的向下调整算法

如果说向上调整算法是从子节点入手,那么向下调整算法就是从父节点入手,父节点和子节点相互比较必然存在一个问题就是,子节点可能只能只有左节点,没有右节点,那我们还要考虑的是两个节点谁小的问题,父节点与两个子节点较小的节点比较,这里可以用到假设法解决。

传进去的参数是数组,堆的有效数据个数,父节点的下标。

这里同样用到while循环,因为是从上往下调整的,所以结束条件应该是child。

为什么是child而不是parent呢?因为调整到最后两层的时候,parent在倒数第二次就不用动了,已经调整结束了,所以向下调整比向上调整有一个明显的优势是在于最后一层不是干涉,时间复杂度会少很多很多,后面再介绍。

假设法找两个子节点中小的那个,为了防止存在越界访问,比如只有一个左孩子,但是child + 1就访问到了右孩子,就越界了,所以child + 1  < size就是为了防止越界访问的。

最后就是进行比较,交换,赋值了。

//堆的向下调整算法
void AdjustDown(HDataType* arr, int size, int parent)
{int child = parent * 2 + 1;//为什么不用child当作循环条件呢?while (child < size){//先找两个孩子中小的那个 假设法if (arr[child + 1] < arr[child] && child + 1 < size){child++;}//交换if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}

2.4 堆的插入

 堆的插入很简单的,就跟顺序表的插入一样的, 无非是最后要保持数据依然是堆而已,因为数据是在最后位置插入的,所以可以用向上算法进行调整,前面就是判断空间够不够,不够扩容就行,就没其他要注意的:

//堆的插入
void HPPush(Heap* php, HDataType x) 
{assert(php);//判断空间是否足够if (php->capacity == php->size){int newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;HDataType* tem = (HDataType*)realloc(php->arr, sizeof(HDataType) * newcapacity);if (tem == NULL){perror("realloc fail");return;}php->arr = tem;php->capacity = newcapacity;}php->arr[php->size++] = x;//插入后依然保持为堆AdjustUp(php->arr,php->size - 1);}

2.5 删除堆顶数据

删除数据都是删除的堆顶数据,那么删除了之后我们该如何保持堆依然是大小堆呢?我们不能直接然后面的数据往前移动一位,这会让堆的数据完全乱套的,结构完全变化了。

前人是思路很是清奇的。我们不妨让第一个数据和末尾的数据进行交换,size--后,堆顶的数据就被删除了,问题是如何保持堆的结构呢?你看,向下调整这不就有大用了,从堆顶一直往下调整呗就,很清奇的这个思路,一下就删除好了。

结合这个思路,如果我们想要找最小,次小的数据是可以模拟这个思路的,后面介绍咯。

当然,没有数据肯定就不能删除了。

//堆的删除数据  删除的是堆顶数据
void HPPop(Heap* php)
{//数据删除之后依然保持为堆assert(php);assert(!HPempty(php));Swap(&php->arr[0], &php->arr[php->size - 1]);php->size--;AdjustDown(php->arr,php->size,0);
}

2.6 建堆

建堆有两个方法,一个是初始化一个堆之后,进行插入数据,调整操作,还有一种就是,初始化的这个过程就进行调整数据,即创建好一个满足堆需求的数组,再拷贝上去就行。

数据给好之后,该赋值的也都要赋值,然后就是调整数据部分,我们可以选择向上调整也可以选择向下调整,至于效率,是向下调整优先的,所以向上调整一般用的是比较少的,后面介绍。

//建堆
void HPInitArray(Heap* php,HDataType* a,int n)
{assert(php);php->arr = (HDataType*)malloc(sizeof(HDataType) * n);if (php->arr == NULL){perror("malloc fail!");return;}memcpy(php->arr, a, sizeof(HDataType) * n);php->capacity = php->size = n;//向上建堆//for (int i = 0; i < n; i++)//{//	AdjustUp(php->arr,i);//}//向下建堆for (int i = (php->size - 1 - 1) / 2; i >= 0; i--){AdjustDown(php->arr, php->size, i);}
}

向下建堆有个要注意的点就是i = php->size - 1 - 1,这是为了防止越界访问,假设堆里面只有9个元素,传进去的i就是4,进入到向下调整之后,child = 9,可是这是size指向的位置,一访问就越界了。


3 建堆的时间复杂度

建堆无非就两种方式,向上建堆和向下建堆,两种方式看似相差不大,实际上时间复杂度是相差较大的,这里就来慢慢分析:

计算时间复杂度之前,我们不妨计算一下树的高度与节点个数之间的关系:
二叉树的节点是以二次方递增的,第一层有2^0个节点,第二层有2^1个节点……第h层有2^(h - 1)次方个节点,那么总节点个数N = 2^0 + 2^1 + 2^3 + …… + 2^(h - 1),这里使用高中的等比求和公式,可以得出,N = 2^h - 1,那么h = log(N + 1)。

3.1 向上建堆的时间复杂度

时间复杂度估算,即是估算每个节点执行多少次操作,第一层的节点,执行调整操作次数至多为0次,第二层1次,第三层2次,第四层3次,第h层 h -1 次。

总的执行次数就是该层的所有节点 * 该层节点执行的至多次数.

F( h ) = 2^0 * 0 + 2 ^ 1 * 1 + 2 ^ 2 * 2 + 2 ^ 3 * 3  + …… +2 ^ (h - 1) * (h - 1),这里利用高中的错位相减,可以得到F(h) = 2^(h - 2) + 2,那么F(N) = ( N  + 1 ) * (log( N + 1) - 2)  + 2。

所以向上建堆的时间复杂度就是O(N) = N * log(N)

3.2向下建堆的时间复杂度

同3.1,向下建堆与向上建堆不一样的是向下建堆止步于倒数第二层,这就是为什么向下建堆算法优于向上建堆算法。

节点向下调整至多调整到倒数第二层,所以第一层的节点执行的次数为h - 1,第二层为h - 2,倒数第二层执行的次数为1次,所以:
F(h) = 2^0 * (h - 1) + 2 ^ 1 * (h - 2) + 2 ^ 2 * (h - 3) + …… + 2 ^ (h-1) * 1,结合高中的数学知识可以得到F(h) = 2^h - h - 3,F(N) = (N + 1) - log( N + 1) - 3.

所以向下建堆的时间复杂度就是O(N) = N - log(N)

这样看来向下建堆的效率远高于向上建堆的效率。


4 堆的排序

堆用来存储数据意义不大,排序倒是有点意思,当我们想让一个数组变成升序,我们是大堆还是小堆呢? 一般来说,小堆就是子节点大于父节点,满足升序,但是实际操作发现哪哪儿都是坑,特别容易改变结构。

面的删除操作有着异曲同工之妙,我们实现升序就选择大堆,讲堆顶数据放在最后,size--就访问不了最大的数据,然后选出第二大的数据,再交换,再size--,再选择第三大的数据,再交换,再size--,重复操作,最后就实现了堆排。

//堆排
void HPsort(HDataType* arr, int size)
{for (int i = (size - 1 - 1); i >= 0 ; i--){AdjustDown(arr,size,i);}int end = size - 1;while (end){Swap(&arr[0], &arr[end]);AdjustDown(arr, end, 0);end--;}
}

感谢阅读!

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

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

相关文章

【第十二届“泰迪杯”数据挖掘挑战赛】【2024泰迪杯】B题基于多模态特征融合的图像文本检索—解题全流程(论文更新)

【第十二届“泰迪杯”数据挖掘挑战赛】【2024泰迪杯】B题基于多模态特征融合的图像文本检索更新&#xff08;论文更新&#xff09; ​ 本节主要更新了论文、训练日志的log数据提取&#xff08;Loss、ACC、RK&#xff09;等数据可视化作图的代码 B题交流QQ群&#xff1a; 4583…

《乱弹篇(26)更好的自己》

俄乌、以巴、中东&#xff0c;烽火连天&#xff0c;持久酣战&#xff0c;搅得地球村住民不得安宁。虽说孰是孰非自有公论&#xff0c;但时评文难写也是评论界的普遍认知&#xff0c;所以今天笔者自觉地绕开时政话题&#xff0c;尽本“人民体验官”义务&#xff0c;推广人民日报…

【BY组态】轻量化web组态编辑器插件

演示地址&#xff1a;http://www.byzt.nethttp://www.byzt.net BY组态是一款非常优秀的纯前端的轻量化【web组态插件工具】&#xff0c;大小只有2M&#xff0c;可无缝嵌入到vue项目&#xff0c;react项目等&#xff0c;由于是原生js开发&#xff0c;对于前端的集成没有框架的限…

计算机网络-TCP/IP 网络模型

TCP/IP网络模型各层的详细描述&#xff1a; 应用层&#xff1a;应用层为应用程序提供数据传输的服务&#xff0c;负责各种不同应用之间的协议。主要协议包括&#xff1a; HTTP&#xff1a;超文本传输协议&#xff0c;用于从web服务器传输超文本到本地浏览器的传送协议。FTP&…

音视频开发之旅(80)- AI数字人-腾讯开源AniPortrait-音频驱动的肖像动画

目录 1、前言 2、效果展示 3、原理学习 4、遇到的问题与解决方案 5、资料 一、前言 一个月前阿里Emo发布&#xff0c;通过音频驱动的非常自然的肖像视频&#xff0c;引起很大反响。具体看下面的视频&#xff0c;但是并没有开源其代码。 这两天腾讯开源了其音频驱动的肖像…

基于微信小程序的付费自习室系统设计与实现,SpringBoot后端+毕业论文(13000字)

介绍 小程序端用户界面和管理员后台界面。用户端界面主要包括注册与登录页&#xff0c;首页、自习室页、我的页、座位预订页&#xff0c;资讯详情页等。管理员页面主要包括登录页、后台管理主页、用户管理页、资讯管理页、咨询管理页等。系统主要的模块包括自习室模块、自习室…

武汉星起航:亚马逊助力全球卖家拓展海外市场,消费潜力巨大

在全球化浪潮的推动下&#xff0c;跨境电商已成为众多企业开拓国际市场的重要途径。作为全球第一大电商平台的亚马逊&#xff0c;凭借其庞大的用户基数和覆盖全球的站点网络&#xff0c;正成为越来越多卖家开展海外业务的首选平台。亚马逊以其强大的销售潜力和影响力&#xff0…

【C++】C++入门第一课(c++关键字 | 命名空间 | c++输入输出 | 缺省参数)

目录 前言 C关键字 命名空间 1.命名空间的定义 A.标准命名空间定义 B.命名空间允许嵌套定义 C.同名命名空间的合并 2.命名空间的使用 加命名空间名称及作用限定符 使用using将命名空间中某个成员引入 使用using namespace命名空间名称引入 C的输入和输出 缺省参数…

【C语言】结构体详解(一)

目录 1、什么是结构体? 2、结构体成分 3、结构体变量的定义与初始化 3.1、结构体变量的三种定义方式 3.2、结构体变量的初始化 4、结构体成员的访问&#xff08;两种方式&#xff09; 4.1、直接访问 4.2、间接访问 5、结构的特殊声明 5.1、不完全声明&#xff08;匿…

STL的string容器

string基本概念 string是C风格的字符串&#xff0c;本质上是一个类。 string 和 char* 的区别 char* 是一个指针&#xff1b; string是一个类&#xff0c;内部封装了 char* &#xff0c;用来管理字符串&#xff0c;是一个 char* 型的容器。 特点 string内部封装了很多成员…

近年来,常见5大软件开发项目管理工具

时代进步&#xff0c;技术进步&#xff0c;汇总下近几年5大常用的软件开发项目管理工具。 1、微软项目管理软件 Microsoft Project&#xff08;或MSP&#xff09;是由微软开发销售的项目管理软件程序。软件设计目的在于协助项目经理制定发展计划、为任务分配资源、跟踪进度、管…

LeetCode 双指针专题

11.盛最多水的容器 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线&#xff0c;使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 说明&#xff1a;你不…

权限提升-Linux系统权限提升篇VulnhubPATH变量NFS服务Cron任务配合SUID

知识点 1、Web或普通用户到Linux-服务安全配合SUID-NFS 2、普通用户到Linux-环境变量配合SUID-$PATH 3、Web或普通用户到Linux-计划任务权限不当-Cron 章节点&#xff1a; 1、Web权限提升及转移 2、系统权限提升及转移 3、宿主权限提升及转移 4、域控权限提升及转移 基础点 …

字体反爬案例分析与爬取实战

字体反爬案例分析与爬取实战 该案例将真实的数据隐藏到字体文件里&#xff0c;即使获取了页面源代码&#xff0c;也没法直接提取数据的真实值。 案例介绍 案例网站https://antispider4.scrape.center/&#xff0c;爬取电影标题、类别、评分等&#xff0c;代码实现如下&#…

腾讯云docker创建容器镜像及仓库

这里为了尽量简单&#xff0c;直接用腾讯云容器版本服务器 腾讯云有自己的镜像加速地址&#xff0c;速度还可以&#xff0c;单纯拉取容器还是够用的 但是当我push容器出现各种各样问题因为网络原因&#xff0c;国内访问docker官方镜像站非常麻烦&#xff0c;所以使用阿里的镜像…

【C++练级之路】【Lv.17】【STL】set类和map类的模拟实现

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C语言》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、红黑树&#xff08;改造版&#xff09;1.1 结点1.2 迭代器1.2.1 operator1.2.2 operator- - 1.3 本体1.…

Vite为什么比Webpack快得多?

Vite为什么比Webpack快得多&#xff1f; 在前端开发中&#xff0c;构建工具扮演着至关重要的角色&#xff0c;而Vite和Webpack无疑是两个备受关注的工具。然而&#xff0c;众多开发者纷纷赞誉Vite的速度之快&#xff0c;本文将深入探讨Vite相较于Webpack为何更快的原因&#xf…

企业知识库搭建不再是难题,靠这几个软件就可以了

在当今知识为王的时代&#xff0c;具备一套强大且实用的企业知识库&#xff08;Knowledge Base&#xff09;已成为提升工作效率、促进团队合作不可或缺的工具。那么&#xff0c;问题来了&#xff0c;我们该如何搭建一套属于自己的知识库呢&#xff1f;今天&#xff0c;我就给大…

WMware虚拟机配置静态IP

注意&#xff1a;如果是克隆的虚拟机&#xff0c;需要先重新生成mac地址&#xff0c;如下图所示 修改配置文件 &#xff1a;/etc/sysconfig/network-scripts/ifcfg-ens33 注意&#xff1a;1. BOOTPROTO设置为static 2.将下面的IPADDR地址替换为你实际要设置的ip地址 3.NAT模式…

前端学习<二>CSS基础——13-CSS3属性:Flex布局图文详解

前言 CSS3中的 flex 属性&#xff0c;在布局方面做了非常大的改进&#xff0c;使得我们对多个元素之间的布局排列变得十分灵活&#xff0c;适应性非常强。其强大的伸缩性和自适应性&#xff0c;在网页开中可以发挥极大的作用。 flex 初体验 我们先来看看下面这个最简单的布局…