文章目录
•🌙知识回顾
大家好啊!💖💖💖我是vince,我们继续进入纯C实现数据结构的坑里来,上一篇文章 vince 详解了树的概念和结构,里面包含了树和二叉树的相关术语及其概念,相对来说是比较好理解的~
vnce今天给大家带来二叉树的堆结构实现,这里的堆学习为后面的堆排序打基础。☀️也希望 vince 的总结在方便后面复习的同时也能给大家带来帮助。
当然在大家看这篇文章之前,vince 还是建议大家先复习复习前面的顺序表和链表,毕竟这里堆的实现以及后面树链式结构的学习也与他们息息相关。
👇👇👇
💘💘💘知识连线时刻(直接点击即可)
🎉🎉🎉复习回顾🎉🎉🎉
详解顺序表
详解双向带头循环链表
💘💘💘学习数据结构当然离不开大量操作练习,因此在这里 🌷给爱学习的小伙伴们推荐个学习、刷题的网站——牛客网,其中面试题应有尽有,真的能够给你带来很好的学习体验。
👇👇👇
爱学习的亲们!🎉🎉🎉请点击我开始注册!学习、刷题🎉🎉🎉
我们先来看看今天学习的思维导图(主要是顺序结构中的堆)


• 🍋知识点一:二叉树的存储结构
二叉树一般可以使用两种结构存储,一种是顺序结构,一种是链式结构。
顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树, 因为不是完全二叉树会有空间的浪费。而现实使用中只有堆才会使用数组来存储。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。
图解完全二叉树的顺序存储:

图解非完全二叉树的顺序存储:

💯文字分析:
非完全二叉树利用顺序结构来存储,在上图中你就会发现有空间浪费现象。因此,一般只有完全二叉树即堆拿顺序结构存储。
二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们介绍学习一般都是二叉链,后面学到高阶数据结构如红黑树等会用到三叉链。
图解分析:

二叉链代码示例:
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
二叉链图解分析:

三叉链代码示例:
typedef int BTDataType;
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
};

• 🍋知识点二:堆的概念及结构
普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常对堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。 这里数据结构中的堆和操作系统中的堆是两个概念,前面还介绍学习过数据结构中的栈和内存中的栈两个概念也不同哈~
如果有一个关键码的集合k = {
k
0
k_{0}
k0,
k
1
k_{1}
k1,
k
2
k_{2}
k2,…,
k
(
n
−
1
)
k_{(n-1)}
k(n−1)},把所有元素按完全二叉树的顺序存储方式存储在一个数组中,并且满足:
k
i
k_{i}
ki <=
k
2
∗
i
+
1
k_{2*i+1}
k2∗i+1 且
k
i
k_{i}
ki <=
k
2
∗
i
+
2
k_{2*i+2}
k2∗i+2(或
k
i
k_{i}
ki >=
k
2
∗
i
+
1
k_{2*i+1}
k2∗i+1 且
k
i
k_{i}
ki <=
k
2
∗
i
+
2
k_{2*i+2}
k2∗i+2),i = 0, 1 , 2……则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
💯小根堆结构图解分析:(树中的父亲都小于孩子)

💯大根堆结构图解分析:(树中的父亲都大于孩子)

堆中某个节点的值总是不大于或不小于其父节点的值;
堆总是一棵完全二叉树。(堆和二叉树的关系)

小根堆向下调整过程图解:


小根堆向上调整过程

这里拿实现小根堆来举例详解。
//堆的初始化
void HeapInit(HP* php)
{
assert(php);
php->a = NULL;
php->size = php->capacity = 0;
}
💯文字分析:
这里的结构还是和之前学过的顺序表结构类似,因此前面也希望大家去复习一下前面的顺序表结构,这样这里就能够很容易理解。
//堆的销毁
void HeapDestory(HP* php)
{
assert(php);
free(php->a);
php->a = NULL;
php->size = php->capacity = 0;
}
堆的插入,不只是插入数据,还要使得插入数据后整体依然保持为堆。因此,此时就需要用到调整算法,这里是建小根堆,在数据插入这里需要用到向下调整算法。
//交换函数
void Swp(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//小堆向上调整算法
//这里算法逻辑思想是二叉树,物理上实际操作的是数组中的数据
void Adjuestup(HPDataType* a, size_t Child)
{
size_t Parent = (Child - 1) / 2;
while (Child > 0)
{
if (a[Child] < a[Parent])//这里条件是Child < Parent 值时交换实现的是小根堆,大于的时候交换实现的是大根堆
{
Swp(&a[Child], &a[Parent]);
Parent = (Child - 1) / 2;
}
else
{
break;
}
}
}
//堆的插入 O(logN)
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
size_t newcapacity = php->capacity == 0 ? 4 : php->capacity * 2;
HPDataType* tmp = (HPDataType*)realloc(php->a, sizeof(HPDataType) * newcapacity);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
php->a = tmp;
php->capacity = newcapacity;
}
}
php->a[php->size] = x;
php->size++;
//以上实在数组尾部插入数据,插入完成后,
//还需要通过算法调整,保证让其还是堆
//以下就是堆的向上调整算法
Adjuestup(php->a, php->size - 1);
}
💯文字分析:
这里是对小堆进行数据插入,用到了向上调整算法,算法逻辑思想是二叉树,物理上实际操作的是数组中的数据。因为插入一个数据之后,可能整体结构就被破坏,所以这里利用向上调整法是为了每插入一个数据后,将整体进行调整使其依然保持一个堆的结构。
💯图解分析:

堆的删除这里有一种更优的方法:将第一个位置的数和最后一个位置的数交换,然后删除最后一个位置的数,最后进行向下调整。这里就用到了向下调整算法,向下调整的目的还是让其保持成一个堆的结构。
//向下调整算法
void AdjuestDown(HPDataType* a, size_t size, size_t root)
{
size_t Parent = root;
size_t LeftChild = Parent * 2 + 1;//计算该parent的左孩子
while (LeftChild < size)
{
//这种方法是保证一直指向左孩子,避免右孩子不存在而出现越界情况
//LeftChild + 1 即指向右孩子
if (LeftChild + 1 < size && a[LeftChild + 1] < a[LeftChild])//如果这里和下面 < 换成 >
//则是大根堆的向下调整算法
{
++LeftChild;//得到右孩子
}
if (a[LeftChild] < a[Parent])
{
Swp(&a[LeftChild], &a[Parent]);
Parent = LeftChild;
LeftChild = Parent * 2 + 1;//始终都指向左孩子
}
else
{
break;
}
}
}
//删除堆顶元素
void HeapPop(HP* php)
{
assert(php);
assert(php->size > 0);//size大于0才能进行删除
Swp(&php->a[0], &php->a[php->size - 1]);//将根位置数据与最后一个位置数据交换
--php->size;
//利用向下调整算法进行调整
AdjuestDown(php->a,php->size,0);
}
💯文字分析:
堆的删除这里利用了一种最优的方法,就是第一个数和最后一个数交换后,删除最后一个数,然后利用向下调整算法进行调整。向下调整算法过程:找出左右孩子中较小的那个然后跟父亲进行比较,如果比父亲小就进行交换,依次向下比较调整。这里实际删除的是原来根位置的数据,即删除堆顶数据。
💯图解分析:

bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
HPDataType HeapTop(HP* php)
{
assert(php);
assert(php->size > 0);//size大于0,才能获取到
return php->a[0];
}
//计算堆内数据个数
size_t HeapSize(HP* php)
{
assert(php);
return php->size;
}
//打印堆内数据
void HeapPrint(HP* php)
{
assert(php);
for (int i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf("\n");
}
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
size_t size;
size_t capacity;//存储空间大小
}HP;
void Swp(HPDataType* p1, HPDataType* p2);//实现子与父值交换
void HeapInit(HP* php);//堆初始化
void HeapDestory(HP* php);//堆的销毁
void Adjuestup(HPDataType* a, size_t Child);//堆的向上调整算法
void HeapPush(HP* php, HPDataType x);//堆的插入,插入之后依然保持其为堆
void HeapPop(HP* php);//删除堆顶数据(最小或最大的数据)
bool HeapEmpty(HP* php);//判断堆是否为空
size_t HeapSize(HP* php);//计算堆中数据个数
HPDataType HeapTop(HP* php);//返回堆顶元素 即最小或最大元素
void HeapPrint(HP* php);//堆数据打印函数
简单举例测试一下:
#include "Heap.h"
void TestHeap1(HP* hp)
{
HeapInit(hp);
HeapPush(hp, 2);
HeapPush(hp, 9);
HeapPush(hp, 3);
HeapPush(hp, 0);
HeapPush(hp, 7);
HeapPush(hp, 10);
HeapPrint(hp);
HeapDestory(hp);
}
int main()
{
HP h;
TestHeap1(&h);
return 0;
}
运行结果:


• 🍋知识点三:堆的应用
//玩一个堆排序(升序)O(N*logN)
void HeapSort(HPDataType* a,size_t size)
{
assert(a);
HP H;
HeapInit(&H);//初始化
for (size_t i = 0; i < size; i++)
{
HeapPush(&H, a[i]);//将数据插入堆中
}
size_t j = 0;
while( !HeapEmpty(&H))
{
a[j++] = HeapTop(&H);//这里是小根堆,因此将堆顶数据依次放入数组中,就是先从小到大排序
HeapPop(&H);//结束后再将堆数据删除
}
HeapDestory(&H);//最终销毁堆
}
int main()
{
int a[] = { 4, 2, 3, 0 , 1, 6, 9, 7 };
HeapSort(a, sizeof(a) / sizeof(a[0]));
for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
💯文字分析:
这里的堆排序实际上是因为建立的小根堆,所以堆顶位置的数据一定是堆内最小的数据,因此每次取出当前的堆顶数据,然后再删除堆顶数据,此时就会再得到一个次小的数据放在堆顶,就这样依次取出,就能得到一个升序的数组,从而达到数据进行升序排序。堆排序属于一个选择排序。时间复杂度:O(N*logN)
运行结果:

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:
1.用数据集合中前K个元素来建堆:前k个最大的元素,则建小堆;前k个最小的元素,则建大堆
2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
时间复杂度:O(K+logk*(N-K)
空间复杂度:O(K)
如果N非常大,K非常小,那基本上就是O(N)
举例如下:
void PrintTopK(int* a, int n, int k)
{
//1.建堆--用a中前K个元素建堆
int* KminHeap = (int*)malloc(sizeof(int) * k);
assert(a);
assert(KminHeap);
for (int i = 0; i < k; ++i)
{
KminHeap[i] = a[i];
}
//建小堆 (注意这里利用向下调整法调整建立小根堆)
for (int j = (k - 1 - 1) / 2; j >= 0; --j)
{
AdjuestDown(a, k, j);
}
//2.将剩余n-k个元素一次与堆顶元素进行比较
for (int i = k; i < n; ++i)
{
if (a[i] > KminHeap[0])
{
KminHeap[0] = a[i];
AdjuestDown(KminHeap, k, 0);
}
}
for (int j = 0; j < k; j++)
{
printf("%d\n", KminHeap[j]);
}
printf("\n");
free(KminHeap);
}
void TestTopK()
{
int n = 10000;
int* a = (int*)malloc(sizeof(int) * n);
assert(a);
srand(time(0));
for (int i = 0; i < n; ++i)
{
a[i] = rand() % 1000000;
}
a[24] = 1000000 + 1;
a[12] = 1000000 + 2;
a[51] = 1000000 + 3;
a[511] = 1000000 + 4;
a[115] = 1000000 + 5;
a[2331] = 1000000 + 6;
a[9999] = 1000000 + 7;
a[766] = 1000000 + 8;
a[4235] = 1000000 + 9;
a[316] = 1000000 + 10;
PrintTopK(a, n, 10);
}
运行结果:

•🌙vince 结语
二叉树和堆的相关概念和结构的介绍和学习到这里就结束啦~但是数据结构的学习之路远没有结束哈!这是非线性结构的开端,后面还有大力输出学习非线性结构。但是在这里,希望大家能够将前面的树的基础概念以及之前的线性结构知识进行回顾复习,使整个纯C数据结构学习是连贯的,这样更加利于我们的学习和理解以及继续拓展。
如果各位大佬们觉得有一定帮助的话,就来个赞和收藏吧,如有不足之处也请批评指正。
学习数据结构当然离不开大量操作练习,因此在这里 🌷给爱学习的小伙伴们推荐个学习、刷题的网站——牛客网,其中面试题应有尽有,真的能够给你带来很好的学习体验。
👇👇👇
爱学习的亲们!🎉🎉🎉请点击我开始注册!学习、刷题🎉🎉🎉
代码不负有心人,98加满,向前冲啊🐬

🎉🎉🎉以上代码均可运行,所用编译环境为 vs2019 ,运行时注意加上编译头文件#define _CRT_SECURE_NO_WARNINGS 1
我一直在尝试在Ruby中实现BinaryTree类,但我得到了stackleveltoodeep错误,尽管我似乎没有在该特定代码段中使用任何递归:1.classBinaryTree2.includeEnumerable3.4.attr_accessor:value5.6.definitialize(value=nil)7.@value=value8.@left=BinaryTree.new#stackleveltoodeephere9.@right=BinaryTree.new#andhere10.end11.12.defempty?13.(self.value==nil)?true:
一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su
TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是
开门见山|拉取镜像dockerpullelasticsearch:7.16.1|配置存放的目录#存放配置文件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/config#存放数据的文件夹mkdir-p/opt/docker/elasticsearch/node-1/data#存放运行日志的文件夹mkdir-p/opt/docker/elasticsearch/node-1/log#存放IK分词插件的文件夹mkdir-p/opt/docker/elasticsearch/node-1/plugins若你使用了moba,直接右键新建即可如上图所示依次类推创建
文章目录概念索引相关操作创建索引更新副本查看索引删除索引索引的打开与关闭收缩索引索引别名查询索引别名文档相关操作新建文档查询文档更新文档删除文档映射相关操作查询文档映射创建静态映射创建索引并添加映射概念es中有三个概念要清楚,分别为索引、映射和文档(不用死记硬背,大概有个印象就可以)索引可理解为MySQL数据库;映射可理解为MySQL的表结构;文档可理解为MySQL表中的每行数据静态映射和动态映射上面已经介绍了,映射可理解为MySQL的表结构,在MySQL中,向表中插入数据是需要先创建表结构的;但在es中不必这样,可以直接插入文档,es可以根据插入的文档(数据),动态的创建映射(表结构),这就
HTTP缓存是指浏览器或者代理服务器将已经请求过的资源保存到本地,以便下次请求时能够直接从缓存中获取资源,从而减少网络请求次数,提高网页的加载速度和用户体验。缓存分为强缓存和协商缓存两种模式。一.强缓存强缓存是指浏览器直接从本地缓存中获取资源,而不需要向web服务器发出网络请求。这是因为浏览器在第一次请求资源时,服务器会在响应头中添加相关缓存的响应头,以表明该资源的缓存策略。常见的强缓存响应头如下所述:Cache-ControlCache-Control响应头是用于控制强制缓存和协商缓存的缓存策略。该响应头中的指令如下:max-age:指定该资源在本地缓存的最长有效时间,以秒为单位。例如:Ca
如何用IDEA2022创建并初始化一个SpringBoot项目?目录如何用IDEA2022创建并初始化一个SpringBoot项目?0. 环境说明1. 创建SpringBoot项目 2.编写初始化代码0. 环境说明IDEA2022.3.1JDK1.8SpringBoot1. 创建SpringBoot项目 打开IDEA,选择NewProject创建项目。 填写项目名称、项目构建方式、jdk版本,按需要修改项目文件路径等信息。 选择springboot版本以及需要的包,此处只选择了springweb。 此处需特别注意,若你使用的是jdk1
前言上一篇我们简要讲述了粒子系统是什么,如何添加,以及基本模块的介绍,以及对于曲线和颜色编辑器的讲解。从本篇开始,我们将按照模块结构讲解下去,本篇主要讲粒子系统的主模块,该模块主要是控制粒子的初始状态和全局属性的,以下是关于该模块的介绍,请大家指正。目录前言本系列提要一、粒子系统主模块1.阅读前注意事项2.参考图3.参数讲解DurationLoopingPrewarmStartDelayStartLifetimeStartSpeed3DStartSizeStartSize3DStartRotationStartRotationFlipRotationStartColorGravityModif
VMware虚拟机与本地主机进行磁盘共享前提虚拟机版本为Windows10(专业版,不是可能有问题)本地主机为家庭版或学生版(此版本会有问题,但有替代方式)最好是专业版VMware操作1.关闭防火墙,全部关闭。2.打开电脑属性3.点击共享-》高级共享-》权限4.如果没有everyone,就添加权限选择完全控制,然后应用确定。5.打开cmd输入lusrmgr.msc(只有专业版可以打开)如果不是专业版,可以跳过这一步。点击用户-》administrator密码要复杂密码,否则不行。推荐admaiN@1234类型的密码。设置完密码,点击属性,将禁用解开。6.如果虚拟机的windows不是专业版,可
IK分词器本文分为简介、安装、使用三个角度进行讲解。简介倒排索引众所周知,ES是一个及其强大的搜索引擎,那么它为什么搜索效率极高呢,当然和他的存储方式脱离不了关系,ES采取的是倒排索引,就是反向索引;常见索引结构几乎都是通过key找value,例如Map;倒排索引的优势就是有效利用Value,将多个含有相同Value的值存储至同一位置。分词器为了配合倒排索引,分词器也就诞生了,只有合理的利用Value,才会让倒排索引更加高效,如果一整个Value不进行任何操作直接进行存储,那么Value和key毫无区别。分词器Analyzer通常会对Value进行操作:一、字符过滤,过滤掉html标签;二、分