目录
[一、 树概念及结构](#一、 树概念及结构)
[2. 完全二叉树](#2. 完全二叉树)
前言
本文介绍二叉树以及堆的认识,以及对于堆的介绍和代码编写。
一、 树概念及结构
1.1树的概念
树是一种非线性的数据结构,由节点(Node)和边(Edge)组成,是由n(n>=0)个有限结点组成一个具有层次关系的集合。树结构具有层次性,通常用于表示具有父子关系的数据。
有一个特殊的结点,称为根结点,根节点没有前驱结点。
除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、......、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,因此树是递归定义的。

要注意:
树中子树不能相交,就如之前说的子树的根节点有且只有一个前驱一样,除了根节点以外,每一个子节点有且只有一个根结点,子树不能有交集,否则就不是树形结构,一棵树N个节点那么它有N-1条边。
1.2树的相关概念

一颗树中每一个部分都有自己的名字,针对上面的一个数对树的相关概念进行解释:
**节点的度:**一个节点包含所有子树的个数成为这个节点的度;例如上面的A节点的度就是6。
**叶节点或终端节点:**度为0的节点;例如上面的LMJK就是叶节点。
**双亲节点或父节点:**若一个节点含有子节点,那么这个节点就是其子节点的父节点;例如A就是B的父节点。
孩子节点或父节点:一个节点含有的子树的根节点称为该节点的子节点;上面B就是A的孩子节点。
兄弟节点:具有相同父节点的节点称为兄弟节点;B、C就是兄弟节点,它们的共同父节点就是A。
**树的度:**一棵树中,最大的节点的度称为数的度;上面就是6。
**节点的吃层次:**从跟开始定义,跟为第1层,跟的子节点为第2层,以此类推;
**树的高度或深度:**树中节点的最大层次;上面就是4层。
**堂兄弟节点:**双亲在同一层的节点互为堂兄弟;如上面的E和F就是互为堂兄弟。
**节点的祖先:**从跟到该节点所经分支上的所有节点;A就是所有节点的祖先。
**子孙:**以某节点为跟的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙。
**森林:**由m(m>0)棵互不相交的数的集合称为森林。
1.3数的表示
树的表示有很多种,这里介绍几种树的表示方法:
1.二叉树表示
二叉树是最基础的树的结构,每一个节点有两个子节点(左子树和右子树),当然还包括一个data用来存储数据。
cpp
// 定义二叉树节点结构
struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
};
2.孩子兄弟表示法
孩子兄弟表示法是构建普通数的常用方式。
cpp
// 多叉树节点结构(孩子兄弟表示法)
struct TreeNode {
int data;
struct TreeNode* firstChild; // 第一个孩子节点
struct TreeNode* nextSibling; // 兄弟节点
};
3.动态数组存储
当然,使用动态数组也可以实现多叉树节点的结构:
cpp
// 多叉树节点结构(动态数组)
struct TreeNode {
int data;
int childCount;
struct TreeNode* children[MAX_CHILDREN];
};
1.4树的实际应用
文件系统管理
文件系统的目录结构通常采用树形组织,根目录为顶层节点,子目录和文件作为子节点。这种结构便于用户导航和管理文件。
数据库索引
B树和B+树是数据库索引的核心数据结构,能够高效支持数据的插入、删除和查询操作,保持对数级的时间复杂度。
网络路由
路由器使用前缀树(Trie)存储IP路由表,快速匹配最长前缀路由规则,优化数据包转发效率。
人工智能决策
决策树通过树形分支模拟人类决策过程,每个节点代表一个判断条件,广泛应用于分类和回归任务。
组织结构管理
企业组织架构常表现为树形,CEO为根节点,各部门经理为子节点,清晰反映汇报关系和职责划分。
XML/HTML解析
DOM树将文档表示为节点树,每个标签作为节点,父子关系反映标签嵌套层级,便于程序分析和操作。
游戏开发
场景图使用树结构管理游戏对象,父子节点关系实现坐标系的层级变换,优化渲染效率。
编译器设计
抽象语法树(AST)表示源代码的语法结构,每个节点对应语言构造,支撑语义分析和代码生成。
二、二叉树概念及结构
2.1概念
一颗二叉树,顾名思义,只能有两个度,所以二叉树是一个有限集合,该集合要么为空,要么每一个根节点加上左右子树组成,同时二叉树有左右之分,次序不能颠倒,因此二叉树是有序树。
2.2特殊的二叉树
1.满二叉树
一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
满二叉树
2. 完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
完全二叉树
2.3二叉树的性质
若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有
个结点.
若规定根节点的层数为1,则深度为h的二叉树的最大结点数是
.
对任何一棵二叉树, 如果度为0其叶结点个数为 , 度为2的分支结点个数为 ,则有 n0= n2+1
若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=
. (ps: 是log以2为底,n+1为对数)。
对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
2.4二叉树的存储结构
二叉树一般有两种存储结构,一种是顺序结构,一种是链式结构。
1.顺序存储
数序存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树的话,中间有空挡,本身数组就是一段连续的存储地址,那么就会有浪费的空间,而现实中只有堆才会使用数组来存储,二叉树顺序存储本身就是物理上的一个数组,在逻辑上就是一颗二叉树。
2.链式存储
用链表来表示一颗二叉树,用链来指示元素的逻辑关系,通常的方法就是链表中每个节点由三个域组成,数据域和左右指针域,左右指针分别用来给出该节点左孩子和右孩子所在的链节点的存储地址,一般都是二叉链,其中红黑树是用到的三叉链。
三、二叉树的顺序结构实现
3.1二叉树的顺序结构
普通的二叉树是不适合使用数组来进行存储的,因为可能会出现大量的空间浪费,只有完全二叉树才更适合顺序结构存储,现实中我们把堆用顺序结构来存储。
3.2堆的概念及其结构
堆是一种特殊的完全二叉树,分为最大堆和最小堆。最大堆中每个节点的值都大于或等于其子节点的值,最小堆中每个节点的值都小于或等于其子节点的值。堆常用于实现优先队列和堆排序算法,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
小跟堆
大根堆
堆的性质:
1.堆中某个节点的值总是不大于或不小于其父节点的值;
2.堆总是一颗完全二叉树;
3.3堆的实现
我们通过实现这些:
cpp
void HeapInit(HP* php);//初始化
void HeapPush(HP* php, HPDataType x);//尾插
void HeapPop(HP* php);//删除头
void AdjustDown(HPDataType* a, int n, int parent);//向下调整
void AdjustUp(HPDataType* a, int child);//向上调整
HPDataType HeapTop(HP* php);//堆顶元素
bool HeapEmpty(HP* php);//检查空
int HeapSize(HP* php);//堆数据的个数
3.3.1结构体定义
cpp
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}HP;
3.3.2初始化
cpp
void HeapInit(HP* php)
{
assert(php);
php->a = (HPDataType*)malloc(sizeof(HPDataType)*4);//分配4空间
if (php->a == NULL)
{
perror("malloc fail");
return;
}
php->size = 0;//当前数据个数
php->capacity = 4;//空间大小
}
3.3.3向上调整(大堆)
cpp
//前提是除了child这个位置,前面的数据都构成堆
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2;
//while(parent>=0)//虽然可以运行,但是不建议,因为有问题,,但还是可以跑
while (child>0)
{
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
函数通过计算父节点索引 parent = (child - 1) / 2
定位当前节点的父节点。循环条件 child > 0
确保不会越界,避免了使用 parent >= 0
可能导致的潜在问题。当子节点值大于父节点值时,调用 **Swap
**交换两者位置,并更新子节点和父节点的索引。如果子节点值不大于父节点值,循环终止,堆性质已满足。
3.3.4向下调整(大堆)
cpp
//前提左右子树都是大堆或者小堆
//向下调整,取大换小
void AdjustDown(HPDataType* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
//选出左右孩子中的大的那一个
if (child+1<n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
break;
}
}
函数接收三个参数:堆数组a
、堆的大小n
、需要调整的父节点索引parent
。初始化时计算左孩子索引:child = parent * 2 + 1
。循环条件为当前孩子节点在堆范围内(child < n
)。在循环体内,先比较左右孩子节点的大小(确保右孩子存在的情况下),选择较大的孩子节点。如果较大的孩子节点大于父节点,则交换两者位置,并继续向下调整。如果父节点已经大于或等于两个孩子节点,则调整完成,退出循环。
3.3.5插入
cpp
void HeapPush(HP* php,HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * php->capacity*2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
AdjustUp(php->a, php->size - 1);
}
3.3.6删除头
cpp
//删除堆顶才有意义,也就是删除根,不断的筛选大根堆小跟堆的根节点,从而实现排序,挑出最大最小的
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
//删除数据
Swap(&php->a[0], &php->a[php->size - 1]);
php->size--;
AdjustDown(php->a,php->size,0);
}
3.3.7获取堆顶元素
cpp
HPDataType HeapTop(HP* php)
{
assert(php);
return php->a[0];
}
3.3.8检查空
cpp
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;
}
3.3.9获取个数
cpp
int HeapSize(HP* php)
{
assert(php);
return php->size;
}
测试
cpp
void HeapSort(int* a, int n)
{
//建堆
//1.向上调整建堆
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);//模拟插入建堆,降序
}
}
//排升序不能建小堆,得建大堆
// 排降序应该建小堆
//排升序第一个数排好了,但是剩下的数关系全乱了
int main()
{
int a[10] = { 2,1,5,7,6,8,0,9,4,3 };//对数组排序
HeapSort(a, 10);
return 0;
}

总结
本文介绍了大小堆的创建以及对于一组数据通过大小堆来实现最后的升序和降序排序。