当前位置: 首页 > news >正文

【数据结构】二叉树基本概念及堆的C语言模拟实现

1.二叉树的基础概念

1.1二叉树的定义

二叉树是一种比较特殊的树型结构,二叉树树的每个结点的度最多有两个且有左右之分就像人的左右手一样,因此我们可以知道二叉树是一颗有序树

结点的左子树的根节点被称为左孩子右边的则为右孩子


1.2特殊二叉树分类

满二叉树:

如果一颗二叉树的每一层都是满的,除了叶子结点外的每个结点都有左右孩子那这颗二叉树则是一颗满二叉树,假如这颗二叉树的层数为n的话那结点的数量就为2^n - 1,如果上面我给的图就是一颗满二叉树

完全二叉树

完全二叉树是由满二叉树引出来的,假设深度这课树有n个结点,所有的结点有1到n开始编号,对于任意结点与他相同深度的满二叉树的编号为1到n的结点位置相同的话,那这颗树就是完全二叉树

其实说白了就是把一颗满二叉树的叶子结点从右向左删除诺干个结点,剩下的就是一颗完全二叉树了:


1.3二叉树的一些性质

(1)如果规定这棵树的根节点层数为1的话,那任意i层的最大结点数量为2^(i - 1)

(2)同样规定这棵树的根节点层数为1假设深度为h,那这棵树的最大结点数量为2^h - 1;

(3)具有n个结点的满二叉树来说,深度h = long 2 (n + 1) (以2为底的对数)

(4)假如这颗二叉树从0开始编号的话,假设这个结点为i号结点,如果父结点存在的话父结点的下标为 (i - 1)/ 2, 如果左孩子存在的话左孩子的下标为 (2 * i )+ 1, 如果右孩子存在的话那右孩子的下标为 (2 * i)+ 2;这个性质很重要,是我们理解顺序存储完全二叉树的基础


1.4二叉树的存储

二叉树的存储分为顺序存储与链式存储两种

顺序存储

普通的二叉树其实是不适合使用数组来存储的,因为会造成很大的空间浪费,比如我这里通过二叉树的性质计算下标来存储的话:

可以看到因为下标的存储不是连续的所以造成了空间的浪费

而当这颗树是一颗完全二叉树时:

可以看到空间有效的被利用起来了,所以完全二叉树更适合使用顺序存储,而下面我们用C语言实现的堆就是完全二叉树的结构,所以我们也使用顺序存储的方式来模拟实现一个堆。但是需要注意的是我们这里的堆是数据结构的堆而不是操作系统管理内存的那个堆区

链式存储

链式存储结构是指用链表来表示一颗二叉树,一般每个结点有三个域:数据域、左右指针域。数据域用于存储该结点的数据,左右指针分别指向该结点的左孩子、右孩子。但这里我们暂时不用


2.堆的概念及模拟实现

2.1堆的概念

前面也有说过堆是一个完全二叉树的存储结构,可以大幅的优化我们的搜索比如实现Trie树的时候假如我们有一个集合用堆来存储{a1, a2, a3, a4, a5}, 那么这个集合在堆中应该满足

根结点最大的堆被称为最大堆或者大根堆, 根节点最小的被称为最小堆或者小根堆:

那小根堆是升序大根堆是降序吗?需要注意的是并不是这样的,因为我们并不限制兄弟之间的大小关系,但我们可以借助堆来快速完成找出前几个最大的问题,这种问题就是top-k问题。也有一种排序叫做堆排序,在后面也会有介绍


2.2堆的C语言模拟实现

OK呀也是又又又又要造轮子,实际在C++中也为我们提供实现好的堆,但我们可以通过模拟来加深一下对堆的理解以及了解一下向下调整算法和向上调整算法

下面是堆的结构体定义和要实现的函数头文件,这里我就以实现一个小根堆为目的了,大根堆的实现方式和小根堆同理:

#pragma once #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <assert.h> typedef int HeapDataType; typedef struct Heap { HeapDataType* a; int size; int capacity; }HP; //初始化与销毁 void HpInit(HP* php); void HpDestroy(HP* php); //建小根堆 void HpPush(HP* php, HeapDataType x); void HpPop(HP* php); //删除元素 HeapDataType HpTop(HP* php); //是否为空 bool HpEmpty(HP* php); int HpSize(HP* php); //交换元素、向上调整算法、向下调整算法 void Sawp(HeapDataType* a, HeapDataType* b); void AdjustUp(HeapDataType* a, int n); void AdjustDown(HeapDataType* a, int prent, int n);

函数实现:

初始化与销毁:

void HpInit(HP* php) { assert(php); php->a = NULL; php->capacity = php->size = 0; } void HpDestroy(HP* php) { assert(php); free(php->a); php->a = NULL; php->capacity = php->size = 0; }

这里的代码都很简单就不赘述了


插入元素与向上调整算法

void HpPush(HP* php, HeapDataType x) { assert(php); if (php->capacity == php->size) { int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity; HeapDataType* tem = (HeapDataType*)realloc(php->a, sizeof(HeapDataType) * newcapacity); if (tem == NULL) { perror("malloc fail !"); return; } php->a = tem; php->capacity = newcapacity; } php->a[php->size] = x; php->size++; AdjustUp(php->a, php->size - 1); }

在插入元素前先判断一下空间是否足够不够就扩容这不是重点,重点是我们该怎么样在插入一个元素之后,继续维护这个堆。这里就该介绍我们的向上调整算法了。

我们插入元素是在堆的叶子结点从左往右依次插入,每插入一个元素时,会破坏堆的有序性,所以为了维护这个堆我们需要从下往上重新调整这个堆让这个堆重新有序。因为我们创建的是一个小根堆,父结点是要比子结点要小的,所以当子结点比父节点要小时就与父结点交换,然后从小往上依次调整维护这个小根堆:

落实在代码上:

void AdjustUp(HeapDataType* a, int n) { int child = n; int parent = (child - 1) / 2; while (child > 0) { if (a[child] < a[parent]) { Sawp(&a[child], &a[parent]); child = parent; parent = (child - 1) / 2; } else//已经完成调整就退出 { break; } } }

删除元素与向下调整算法

void HpPop(HP* php) { assert(php); assert(php->size > 0); Sawp(&php->a[php->size - 1], &php->a[0]); php->size--; AdjustDown(php->a, 0, php->size); }

想要删除堆中的元素删除的是堆的根结点,删除的方式就是将根节点与堆的最后一个叶子结点做交换,此时堆中除了根节点的位置外其他的结点还是符合小根堆的,所以我们需要重新从上往下的调整堆让堆变成重新有序,与向上调整相似,因为我们要创建的是小根堆所以当子孩子比该结点小的话就把他交换上来:

落实在代码上:

void AdjustDown(HeapDataType* a, int prent, int n) { int child = prent * 2 + 1; while (child < n) { if (child + 1 < n && a[child + 1] < a[child])//先判断是否越界 { child++; } if (a[child] < a[prent]) { Sawp(&a[child], &a[prent]); prent = child; child = prent * 2 + 1; } else { break; } } }

返回元素、判空、返回个数

//返回元素 HeapDataType HpTop(HP* php) { assert(php); assert(php->size > 0); return php->a[0]; } //判空 bool HpEmpty(HP* php) { assert(php); return php->size == 0; } //返回元素个数 int HpSize(HP* php) { assert(php); return php->size; }

这三个函数都非常简单就不多赘述了,其实这整个模拟下来代码还是比较的简单的,主要是加深了一下对堆的理解,尤其是体现在向上、向下调整算法那里就体现了堆的核心思想


http://www.jsqmd.com/news/731943/

相关文章:

  • 告别混乱!用Qt的SUBDIRS管理多项目工程,保姆级配置流程分享
  • 告别触控失灵!手把手教你用ADB命令修复Scrcpy连接小米/鸿蒙手机(附一键脚本)
  • ChatPilot:模块化本地AI对话应用框架的设计、部署与深度定制指南
  • 2026 神马影视 8.8 新版源码 架构性能全新升级
  • 告别报错!手把手教你搞定Matlab/Simulink中Embedded Coder的6个关键配置(含可变信号、主函数设置)
  • Nintendo Switch大气层系统终极指南:从零构建自定义固件的完整解决方案
  • 嵌入式电源设计避坑指南:基于WL2866D的I2C控制实战,这些细节错了真没输出
  • 如何用Python轻松下载B站4K大会员视频:完整免费教程
  • 告别重复劳动:用Python自动化工具解放你的双手
  • 别再只用QLabel显示图片了!PyQt5 QImage像素级操作保姆级教程(附OpenCV/Numpy互转代码)
  • Maven精讲
  • 5分钟上手MouseTester:你的鼠标性能测试专家指南
  • 如何在3分钟内免费为视频添加专业字幕:VideoSrt完整指南
  • 2026年过半,ZDNET读者购买最多的热门产品清单来了!
  • R语言做LLM偏见检测,你还在用`prop.test()`?——2024最新面试真题:多组敏感属性嵌套Logistic回归+多重比较校正(Bonferroni vs. BH)实战对比
  • S32K3双核MCU实战:手把手教你用MCAL配置两路独立LIN通信(附中断调试代码)
  • 2026北京国际车展:AI上车、算力军备赛,汽车行业格局重塑!
  • 专业音频路由解决方案:Synchronous Audio Router如何解决Windows多应用音频同步难题
  • Nintendo Switch游戏文件管理终极指南:NSC_BUILDER完整教程
  • ComfyUI-AnimateDiff-Evolved终极指南:5个核心技巧打造专业级AI动画
  • 观察 Taotoken 在全球多个节点下的 API 调用延迟与稳定性表现
  • 2026最权威的五大降重复率工具实测分析
  • 突破网盘下载瓶颈:LinkSwift直链解析工具的技术革新与应用实践
  • RT-Thread FinSH控制台保姆级使用指南:从串口连接到自定义命令实战
  • 微信免费去水印小程序推荐:2026 实测哪个安全好用?微信里去水印的小程序怎么选? - 科技热点发布
  • 终极指南:用QKeyMapper在Windows上实现跨设备按键映射
  • 解决中文字体版权与性能难题的开源方案:思源宋体TTF实战深度应用
  • 如何彻底清理Mac应用残留文件?Pearcleaner为你提供终极解决方案
  • Cadence Allegro设置stroke手势命令(以显示网络飞线为例子)
  • 从Numpy老手到PyTorch新手:关于Tensor的reshape,你需要切换的3个思维定式