c语言实现数据结构---树的基础概念

本文详细介绍了树这一非线性数据结构的基本概念,包括满二叉树、完全二叉树等特殊形态,以及节点的度、叶节点等关键概念。同时探讨了树的多种存储方式,如左孩子右兄弟表示法和双亲表示法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树的概念

通过之前的学习,我们知道顺序表,链表,栈,队列这4个数据结构,但是这4个数据结构都有着相同的性质就是都是线性的数据结构,那么这里的线性表示的意思就是每个元素都是一对一的关系,最直观的体现就是我们的链表,我们通过第一个元素能够找到第二个元素,通过第二个元素就能找到第三个元素,还有我们的这里的顺序表,第一个元素后面就紧跟着第二个元素,第二个元素后面就紧跟着第三个元素,那么我们这里的树他就跟上面的几个结构完全不一样,树是一种非线性的数据结构,也就是说它里面的元素可以是一个一对多的结构,而且它是由n(n>=0)个有限结点组成一个具有层次关系的集合。比如说这样:
在这里插入图片描述
那么我们这个图中的A就是一个一对多的关系,它一个节点就可以找到另外三个节点,还有这里的B他一个节点也可以找到两外两个节点,那么我们这里把它叫做树就是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。我们这里把每个元素都称之为节点,那么我们这里树中就有这么一个特殊的节点叫做根节点,也就是这里的A,这个根节点他是没有前驱的,这里前驱的意思就是一个指向它的元素比如说这里的j的前驱就是E,既然有前驱的话,那么也就有后驱,那么我们这里的后驱的意思就是一个元素指向的下一个元素,那么我们这里的C他就指向了一个G,这里的G就是后驱。大家仔细地观察这个图就可以发现我们这里除了A这个节点其他地元素都有指向,E指向J,而B指向E,A又指向B,但是没有哪个元素能够指向A,所以这里地A就是根节点,除根节点外,其余结点被分成M(M>0)个互不相交的集合T1,T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继因此,树是递归定义的。但是这里大家要注意的一点就是我们这里树的结构中,子树之间是不能有交集的,比如说
在这里插入图片描述
那么这里的两个子树之间就有交际,所以这个就不叫树,同样的道理
在这里插入图片描述
在这里插入图片描述
这两个图片都不是树,那么这里大家根据上面的讲解应该能够发现我们这里的树,有这么几个性质:

  1. 子树之间是不相交的。
  2. 除了根节点外,每个节点之间有且仅有一个父节点。
  3. 一颗含有N个节点的的树含有N-1条边。

然后我们这里的树还有这么几个特殊的树,第一个就是满二叉树:这个树有K层,其中这K层所有的元素都是满的。比如说下面的图片中的树:
在这里插入图片描述
这就是一个满二叉树,他有4层并且每层的元素都是满的,那我们再来看这个图片
在这里插入图片描述
这就不是一个满二叉树,因为这个树有三层但是这个树的第三层却没有满,如果是满二叉树的话这里的第三层应该有4个元素,但是这里的第三层却只有3个元素。第二个就是完全二叉树:这个树的特征就是前k-1层都是满的,第k层可以满也可以满,但是第k层从左到右必须得是连续的,我们来看看完全二叉树的图片:
在这里插入图片描述
如果第K层不连续的话那么就不是完全二叉树,大家可以再来看看上面的图片:
在这里插入图片描述
这里第三层中的元素3到元素5之间就缺少了一个元素,元素2的右节点为空所以它不是完全二叉树,那么这里大家注意一下这里的左节点和右节点不能混为一谈,你要想连续的话就必须得先有左节点再有右节点。好!看到这里想必大家知道了什么是满二叉树什么是完全二叉树,那这里大家再思考一个问题,一个完全二叉树有k层,那么它的节点数目是多少呢?首先我们来看看数目最多的情况,我们知道完全二叉树的最后一层可以是满的,所以我们这里的完全二叉树就可以当作满二叉树来计算k层的满二叉树的节点数目,首先第一层节点的数目是1 ,也就是2的0次方,第二层节点的数目是2也就是2的1次方,第三层的节点数目是4也就是2的2次方,那么我们这里依次类推的话我们这里就知道第k层的节点的数目就是2的k-1次方,那么这整个树的节点数目就是2的0次方一直加到2的k-1次方,那最终的结果就是2的k次方-1,那么这里计算的结果就是我们k层完全二叉树含有的最多的节点数目,那最少呢?我们知道既然这个树他含有k层,那么这个树第k层是不是最少得有一个节点啊,好我们知道第k层至少有一个元素,那么前k-1层是满的也就有2的k-1次方-1个元素,然后第k层有一个元素那么这里最少就有2的k-1次方个元素。所以我们这里的k层的完全二叉树的节点个数的范围就是2的k-1次方到2的k次方-1。

树的相关概念

我们这里来看看与树有关的概念,我们这里就以这个图来讲解以下的概念。
在这里插入图片描述

  1. 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的度为6。
  2. 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I…等节点为叶节点。
  3. 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G…等节点为分支节点。
  4. 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点。
  5. 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点。
  6. 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点,那么这里的兄弟节点指的是亲兄弟也就是得有相同的父节点。
  7. 树的度:一棵树中,最大的节点的度称为树的度; 如上图:树的度为6。
  8. 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推。
  9. 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4。
  10. 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点。
  11. 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先。
  12. 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  13. 森林:由m(m>0)棵互不相交的树的集合称为森林;

树的储存

我们这里我们知道了树的样子,以及与树有关的一些概念,那么我们这里如何来实现树的储存呢?大家看到上面的形式就能够联想到我们之前学的链表,说我们这里用链表的思路来实现这个树可不可以呢?我们创建一个结构体,这个结构体里面装着一个变量用来存放我们的数据,然后再装几个指针用来指向我们的孩子节点,那么我们的代码就是这样:

struct TreeNode
{
	int data;
	struct TreeNode* child1;
	struct TreeNode* child2;
	//.....
};

但是大家看到这个代码的时候应该已经发现了一个问题就是:我们好像不知道这个结构体中该含有多少个指向子孙的结构体的指针,所以我们这里以这种方式进行的存储的话多少是有点麻烦的,那么如果我们这里如果知道要存放多少个指针呢?比如说5的话,那么我们这里的代码就如下:

struct TreeNode
{
	int data;
	struct TreeNode* child1;
	struct TreeNode* child2;
	struct TreeNode* child3;
	struct TreeNode* child4;
	struct TreeNode* child5;
};

那有小伙伴们就要说啊我们这里既然是五个相同元素,那为什么不把他写成数组的形式呢?所以我们这里就可以对其进行一下改进,那么代码的形式就如下:

#define N 5
struct TreeNode
{
	int data;
	struct TreeNode* ChildArr[N];
	int childsize;
};

那么我们这里是一个静态的数组,这里的性质我们就可以类比成我们的静态的顺序表,多了可能会浪费,少了又不够用,所以我们这里就可以对其做出一些改进将其改成动态形式,那么我们的代码就如下:

struct TreeNode
{
	int data;
	struct TreeNode** ChildArr;
	int childsize;
	int childcapacity;
};

看到这里可能会对这个结构体中的二级指针有那么点点的疑惑,说这里为什么是一个二级指针啊,应该是一个一级指针啊,我们之前的顺序表的实现用的就是一级指针啊,那么这里我们就得这么想,我们之前的顺序表中动态开辟的空间是用来存储数据的,但是我们这里的动态开辟的空间他不是用来存储数据的,他是用来存储下一个元素的地址的,而我们知道动态开辟的空间他是通过指针来进行维护的的,那么这里指针指向的元素是地址的话,是不是就得是一个二级指针啊,所以我们这里就是地址的地址,也就是一个二级指针。好我们这里解释清楚了为什么这里是二级指针之后我们就得想一个问题就是我们以这样的形式来进行存储数据是否合理呢?我们这里直接在结构体里面放进去了多个指针,这些指针之间是没有任何的联系的,那我们以后在进行插入删除打印操作的时候是不是就非常的麻烦啊,对吧我们得到处找这些指针在哪里?所以我们这里就得对这些指针经行一下改进,我们得让同一级别的指针变的有联系起来,那么这里就有了这样的结构,叫做左孩子右兄弟表示法,这个表示的方法就是每一个双亲节点指向都只指向我们最左边的孩子,然后我们最左边的孩子就指向他的兄弟节点,那么我们这里就可以通过这个图来理解我们这里的结构:
在这里插入图片描述

首先我们的根节点A指向这里最左边的节点B,然后我们的节点B再来指向他的兄弟节点C,然后因为这一辈就只有两个节点所以我们这里的C就指向空,然后同样的道理,我们这里的B再指向他的最左边的子节点D,然后子节点D再指向它的下一个兄弟E,E再指向它的下一个兄弟F,然后因为F后面没有兄弟了所以它的指向就是空,那么再依次往下推都是同样的道理,那么这种遍历的方式我们还可以以这样的方式来理解,在原来条件十分艰难的时期,我们的爸妈他们所能创造的条件都十分的有限,他们两个人就只能供一个孩子上学读书,但是他们又想让所有的孩子都享受教育,于是他们就先将老大养大,让他读书赚钱,然后老大参加工作赚钱然后把老大赚的钱用来抚养老二,然后老二读书长大赚钱之后,再将老二赚的钱用来抚养老三,这样依次类推就有了我们上面说的结构,那么我们这样的结构形式的结构体就长这样:

typedef int DataType;
struct Node
{
	struct Node* first_child;//左边的第一个孩子的节点
	struct Node* PNextBrother;//指向其下一个兄弟节点
	DataType data;//结点中存的数据
};

那么我们有这样的结构我们在打印数据的时候就非常的简单,我们来写一个打印一个双亲节点中所有孩子节点的数据的函数,首先我们这个函数需要的参数就是就传过来一个双亲节点,然后在这个函数里面先判断一下我们这个传过来的参数是不是一个空指针,如果是空指针的话我们就直接结束这个函数,如果不是空的话我们这里就先找到这里的最左边的孩子,然后我们再通过循环不停的打印下一个元素,然后再将指针指向下一个元素,

void printfTree(struct Node* parent)
{
	if (parent == NULL)
	{
		return ;
	}
	struct Node* cur = parent->first_child;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->PNextBrother;
	}
}

那么我们这里还有一种表示就是双亲表示法,这个表示法的逻辑就是一个数组用来存储树中的数据,然后再创建一个数组用来来存储对应位置的元素的双亲的下标,比如说这个树
在这里插入图片描述
那么我们在存储元素的时候就单独拿出一个数组出来:
在这里插入图片描述
然后我们再创建一个数组,这个数组就来存储上面数组对应位置的双亲的下标,比如说这里的B它的双亲就是A,而A的下标就是0,所以我们在另外一个数组对应的下标的位置存放的就是0,比如说这里的G,他的双亲就是这里的C,而c的下标是2,所以我们在G对应位置的数组的地方存放的就是2,而我们知道根节点是没有双亲的,所以我们这里在A对应位置存放的数据就是-1用来表示没有双亲,那么我们另外一个数组就是这样:
在这里插入图片描述
那么我们以这样方式来表示树的话最大的特点就是能够很快的通过孩子来找到祖先。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶超凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值