【数据结构】C语言实现二叉树的基本操作——二叉树的遍历(先序遍历、中序遍历、后序遍历)

C语言实现二叉树的基本操作

  • 导读
  • 一、二叉树的遍历
  • 二、先序遍历
  • 三、中序遍历
  • 四、后序遍历
  • 五、结点序列
  • 六、递归算法与非递归算法的转化
  • 结语

封面

导读

大家好,很高兴又和大家见面啦!!!

通过前面的介绍,我们已经认识了二叉树的逻辑结构和存储结构。在今天的内容中,我们将会开始介绍二叉树的最后一个要素——二叉树的基本操作。

基本操作作为数据结构的三要素之一,对数据结构的具体实现是必不可少的。对于任何一种数据结构而言,都需要有以下几种基本操作:

  1. 创建与销毁
    • Init/Creat(&T)——初始化会创建一个数据结构T;
    • Destroy(&T)——销毁一个数据结构T;
  1. 增加与删除
    • Insert(&T,x)——将元素x添加到数据结构T中 ;
    • Delete(&T,&x)——将元素x从数据结构T中删除;
  1. 查找与修改
    • Search(T,x)——在数据结构T中查找元素x;
    • Modify(&T,&x,y)——在数据结构T中将元素x修改为y;

这些基本操作我们可以用六个字来概括——创销增删查改。在这六种基本操作的基础上,不同的数据结构又会衍生出其独特的基本操作:

  • 动态顺序表中会有修改表长的操作——IncreaseSize(&L,len)
  • 链表中增加元素的操作根据增加的位置不同衍生出了头插和尾插的基本操作——HeadInsert(&L,len)/TailInsert(&L,len)
  • 栈中查找操作根据删除的位置限制变成的获取栈顶元素的操作——GetTop(S,&x)
  • 队列中根据增加与删除位置的限制变成了入队和出队操作——EnQueue(&Q,x)/DeQueue(&Q,&x)
  • 串中查找操作从查找某一个元素变成了串定位操作——Index(S,T)

类似于上述这些独属于某一种数据结构的基本操作还有很多这里就不再一一列举。

从今天开始,我们将会介绍一些独属于二叉树的基本操作以及该操作的C语言实现。在这之前我们先要确定一下今天的内容中我们需要选择哪一种存储结构来进行介绍。在上一篇内容中我们也介绍过二叉树的两种存储结构,对于二叉树而言,不同的存储结构,其基本操作的具体实现也是有所区别的:

  • 顺序存储结构:适用于完全二叉树和满二叉树的存储;
  • 二叉链表:适合于已知根结点需要对其左右子树进行操作的场合;
  • 三叉链表:适合于需要频繁对父结点进行操作的场合;

在今天的内容中,我们会以二叉链表为例来介绍二叉树中这些基本操作的具体实现,对应的数据类型如下所示:

//二叉树的数据类型
typedef int ElemType;
typedef struct BiTreeNode {
	ElemType data;//数据域
	struct BiTreeNode* lchild, * rchild;//指针域
}BTN, * BTL;
//BTN——二叉链表结点类型
//BTL——二叉链表类型

下面我们就来开始进入今天的内容吧!!!

一、二叉树的遍历

从二叉树的定义中我们可以得知,一棵二叉树无非就两种形态——空二叉树和非空二叉树:

  • 空二叉树:二叉树中的结点数量为0;
  • 非空二叉树:二叉树中的结点数量大于0;

在非空二叉树中任意一棵子树我们都可以将其视作作为一棵由左子树、根结点和右子树三部分组成的二叉树。只不过不同的子树其左右子树会有不同:

  • 度为0的子树其左右子树均为空树;
  • 度为1的子树其左右子树有一棵为空树;
  • 度为2的子树其左右子树都为非空二叉树;

借助这种递归定义,我们在遍历一棵二叉树时,就可以看做通过遍历二叉树中的每一棵子树从而完成遍历一棵二叉树。如下所示:
二叉树的遍历
在上图展示的例子中我们可以看到,对于一棵结点数量为3的二叉树而言,我们就可以将其看做一棵分别有这三个结点为根结点的结点数量为3的二叉树所组成的一棵二叉树。

此时如果我要遍历这一棵二叉树,则相当于我要遍历这三棵子树,并且这三棵子树中都只有左孩子、根结点、右孩子3个结点。根据遍历这些子树的先后顺序不同,于是便衍生出了3种遍历方式:

  1. 先序遍历(先根遍历):PreOrder(T)——从二叉树的根结点开始,按照根结点、左子树、右子树的顺序完成遍历;
  2. 中序遍历(总根遍历):InOrder(T)——从二叉树的左子树开始,按照左子树、根结点、右子树的顺序完成遍历;
  3. 后序遍历(后根遍历):PostOrder(T)——从二叉树的左子树开始,按照左子树、右子树、根结点的顺序完成遍历;

对于树形结构而言,它本身是一种递归型的数据结构,因此其基本操作的实现都可以通过递归的方式来完成,下面我们就来探讨一下这三种遍历算法以及其C语言的实现;

二、先序遍历

先序遍历又称为先根遍历,意思是优先访问根结点。在先序遍历中,算法的整个流程大致分为三步:

  1. 访问根结点
  2. 访问左子树
  3. 访问右子树

以上述例子为例,在先序遍历中,我们整个的遍历顺序如下所示:

先根遍历
在第一遍历中,我们先访问的是以元素 a 1 a_1 a1为根结点的这棵子树,此时在这棵子树中我们先访问的是根结点 a 1 a_1 a1,然后再访问其左子树和右子树;

在第二次遍历中,我们访问的是根结点 a 1 a_1 a1的左子树,即以 a 2 a_2 a2为根的子树,此时在这棵子树中我们同样先访问的是根结点 a 2 a_2 a2,然后再访问其左右子树,只不过此时 a 2 a_2 a2这棵子树的左右子树都为空;

在第三次遍历中,因为根结点 a 1 a_1 a1的左子树 a 2 a_2 a2的左右子树为空,所以第三次遍历访问的是根结点 a 1 a_1 a1的右子树,即以 a 3 a_3 a3为根的子树,此时在这棵子树中我们同样先访问的是根结点 a 3 a_3 a3,然后再访问其左右子树,但是由于 a 3 a_3 a3这棵子树的左右子树都为空,因此结束访问。

从上述的遍历过程中我们不难发现,原先我们是需要对整棵二叉树进行访问的,但是在实际的过程中,我们只访问了该二叉树的三棵子树的根结点。这一整个过程我们可以大致总结为:
从遍历整棵树— > 遍历每一棵子树— > 访问每棵子树的根结点。 从遍历整棵树—>遍历每一棵子树—>访问每棵子树的根结点。 从遍历整棵树>遍历每一棵子树>访问每棵子树的根结点。

从遍历整棵树到最后的访问每棵子树的根结点,这种将原先的大目标逐步拆解为一个个相同的小目标的过程实际上体现的就是递归的思想,如果将这一过程用代码表示则是:

//先序遍历
void PreOrder(BTN* root) {
	if (!root)
		return;
	visit(root->data);//访问根结点
	PreOrder(root->lchild);//遍历左子树
	PreOrder(root->rchild);//遍历右子树
}

可能有朋友会对递归算法比较模糊,我们先来简单的回顾一下递归的相关知识点:

递归:通过在函数体中调用自身来重复完成同一件事。
在函数递归中有两点需要注意:

  1. 递归算法中需要添加一个限制条件,防止栈溢出的问题
  2. 每一次递进后都应该越来越接近这个限制条件

在回顾完了递归的知识点后,接下来我们就来看一下在二叉树的先序遍历中是如何走完整个递归流程的:

递归流程
从上图演示中我们可以看到,每一次进入函数后都会进行一次条件判断,判断传入的结点是否为空,这个就是控制递归结束的限制条件:

  • 当传入的结点为空时,开始回归;
  • 当传入的结点非空时,继续往后执行;

算法中的visit函数表示的是访问根结点,函数的具体内容可以为打印结点中存放的数据,可以读取结点中存放的数据……

在访问完根结点后,算法会通过递归开始遍历左子树。遍历左子树时会不断地重复条件判断、调用visit和递归遍历左子树的过程,直到访问到的左子树为空树才会开始回归;

在左子树完成回归后,则会进入右子树的递归遍历。遍历右子树的过程同样是不断重复条件判断、调用visit和递归遍历左子树以及左子树遍历完成后的右子树递归遍历,直到右子树为空树才会开始回归;

整个算法过程以及思路并不难理解,这里需要我们关注的是递归调用的算法思路以及整个递归调用的过程分析,建议大家可以自己手动绘制一下递归调用的流程图,这里我可以给大家一个流程图作为参考:

简易递归流程
这里展示的是简易的递归流程图,学会画这种递归流程图能够帮助我们更好的理解递归的算法,建议大家下去一定要尝试着动手画一下。当然如果能将这个简易的流程图改为完整的流程图,我相信当你将整个流程如给画完的同时,能够对递归的理解再上升一个台阶。

三、中序遍历

中序遍历又称中根遍历,意思是在中间访问根结点。在中序遍历中,算法的整个流程同样分为三步:

  1. 访问左子树
  2. 访问根结点
  3. 访问右子树

与先序遍历相似,只不过在中序遍历中,我们是先访问的左子树再访问的根结点,对应的代码如下所示:

//中序遍历
void InOrder(BTN* root) {
	if (!root)
		return;
	InOrder(root->lchild);//遍历左子树
	visit(root);//访问根结点
	InOrder(root->rchild);//遍历右子树
}

从代码中我们可以看到,在中序遍历中,对根结点的访问是在左子树开始回归后执行的,因此中序遍历访问的第一个结点一定是二叉树中第一棵左子树为空树的子树根结点,如下所示:

中根遍历
中根遍历的递归简易流程图如下所示:
递归简易流程
之所以在中序遍历中第一个访问的结点为左子树为空树的子树根结点,是因为在进行中序遍历时,算法先通过左子树递归遍历一直往下找,直到左子树为空才会开始回归,此时我们也只能得到该子树的左子树为空这个结论,并不能保证该子树的右子树也为空,如下所示:

中根遍历2
从这个例子中可以看到对于 a 2 a_2 a2这棵子树而言,它的左子树为空,右子树并不为空,但是通过中序遍历的算法,因为根结点的访问是在右子树之前,因此 a 2 a_2 a2要早于 a 4 a_4 a4进行访问。

四、后序遍历

后序遍历又称为后根遍历,意思是最后访问根结点。在后续遍历中,算法的整体流程还是分为三步:

  1. 访问左子树
  2. 访问右子树
  3. 访问根结点

后序遍历与先序遍历相比,也仅仅只是将访问根结点的顺序放在了最后,代码如下所示:

//后序遍历
void PostOrder(BTN* root) {
	if (!root)
		return;
	PostOrder(root->lchild);//遍历左子树
	PostOrder(root->rchild);//遍历右子树
	visit(root);//访问根结点
}

在二叉树的后序遍历中,由于根结点的访问是在右子树开始回归后执行,因此二叉树访问的一定是左子树中的第一个叶结点,如下所示:

后根遍历
后序遍历的递归简易流程图如下所示:

递归简易流程
之所以是左子树中的第一个叶结点,当开始进行左子树遍历回归时,说明该结点的左子树一定为空树,当进行右子树遍历回归时,说明该结点的右子树一定为空树,一个结点的左右子树都为空树那就说明该结点为叶结点,如下所示:

后根遍历2
在这个例子中我们可以看到,通过左子树的递进找到了第一棵左子树为空树的左子树,其对应的根结点为 a 2 a_2 a2

在进行左子树回归后即刻执行的是右子树递归遍历,因此算法会继续遍历 a 2 a_2 a2这棵子树的右子树,即 a 4 a_4 a4这棵子树,而 a 4 a_4 a4这个结点为该二叉树中左侧第一个叶结点,因此其对应的左右子树都为空;

当开始遍历该子树时,还是先从该子树的左子树开始遍历,由于这棵子树的左子树为空,因此函数进行了左子树的回归;之后就是遍历该子树的右子树,由于这棵子树的右子树也为空,因此函数进行了右子树回归;最后才会执行访问根结点的操作。

五、结点序列

在这三种遍历算法中,如果我们去掉访问根结点的操作的话,会是怎样的结果呢?如下所示:

//先序遍历
void PreOrder(BTN* root) {
	if (!root)
		return;
	PreOrder(root->lchild);//遍历左子树
	PreOrder(root->rchild);//遍历右子树
}
//中序遍历
void InOrder(BTN* root) {
	if (!root)
		return;
	InOrder(root->lchild);//遍历左子树
	InOrder(root->rchild);//遍历右子树
}
//后序遍历
void PostOrder(BTN* root) {
	if (!root)
		return;
	PostOrder(root->lchild);//遍历左子树
	PostOrder(root->rchild);//遍历右子树
}

可以看到,此时这三种算法是一样的,因此我们可以得到结论,二叉树的三种遍历算法的区别在于对根结点的访问的时机不同:

遍历算法遍历顺序第一个访问结点
先序遍历根结点—>左子树—>右子树二叉树的根结点
中序遍历左子树—>根结点—>右子树左子树中第一个左子树为空树的结点
先序遍历左子树—>右子树—>根结点左子树中的第一个叶结点

算法的这种区别会导致一个结果——通过算法得到的结点序列会有不同。

那什么是结点序列呢?

简单的理解就是由结点数量为n的二叉树的结点中存储的数据所组成的长度为n的序列。而这个序列是由结点访问的先后顺序决定的。如下所示:

结点序列
在这棵二叉树中,如果我们按照三种遍历算法对该树进行遍历,那么得到的结点序列分别为:

  • 先根遍历: a 1 a 2 a 4 a 7 a 3 a 5 a 6 a 8 a_1a_2a_4a_7a_3a_5a_6a_8 a1a2a4a7a3a5a6a8
  • 中根遍历: a 4 a 7 a 2 a 1 a 5 a 3 a 6 a 8 a_4a_7a_2a_1a_5a_3a_6a_8 a4a7a2a1a5a3a6a8
  • 后根遍历: a 7 a 4 a 2 a 5 a 8 a 6 a 3 a 1 a_7a_4a_2a_5a_8a_6a_3a_1 a7a4a2a5a8a6a3a1

有朋友可能会好奇,这些序列是如何得到的呢?下面我们就以中根遍历为例来介绍一下手算获取结点序列的过程:

中根遍历序列的手算步骤

  1. 根结点对应的子树的序列为 a 2 a 1 a 3 a_2a_1a_3 a2a1a3
  2. a 2 a_2 a2为根的左子树对应的序列为 a 4 a 2 a_4a_2 a4a2
  3. a 4 a_4 a4为根的左子树对应的序列为 a 4 a 7 a_4a_7 a4a7
  4. a 7 a_7 a7为根的右子树对应的序列为 a 7 a_7 a7
  5. 根结点对应的左子树序列为 a 4 a 7 a 2 a_4a_7a_2 a4a7a2
  6. a 3 a_3 a3为根的右子树对应的序列为 a 5 a 3 a 6 a_5a_3a_6 a5a3a6
  7. a 5 a_5 a5为根的左子树对应的序列为 a 5 a_5 a5
  8. a 6 a_6 a6为根的右子树对应的序列为 a 6 a 8 a_6a_8 a6a8
  9. a 8 a_8 a8为根的右子树对应的序列为 a 8 a_8 a8
  10. 根结点对应的右子树的序列为 a 5 a 3 a 6 a 8 a_5a_3a_6a_8 a5a3a6a8
  11. 二叉树的中序结点序列为 a 4 a 7 a 2 a 1 a 5 a 3 a 6 a 8 a_4a_7a_2a_1a_5a_3a_6a_8 a4a7a2a1a5a3a6a8

这整个手算的过程可以总结为以下几步:

  1. 先找到根结点对应的子树的序列
  2. 通过遍历算法找到其左右子树对应的序列
  3. 合并子树序列

大家可以按照我这个步骤来自己手算一下先序遍历和后序遍历的结点序列。

六、递归算法与非递归算法的转化

序列问题大家有没有一种熟悉的感觉呢?我们好像有在哪里遇到过一样,是在哪里呢?

没错在第三章——栈、队列与数组这个章节我们有提到过序列的问题:

  • 在栈中,根据入栈和出栈的顺序不同,我们能够得到不同的出栈序列;
  • 在队列中,根据入队和出队的顺序不同,我们能够得到不同的出队序列;

可以看到,不管是在栈中还是在队列中,其获得的序列都是与入和出的顺序相挂钩的,那如果我们把二叉树的递进看做入,访问根结点看做出,那是不是代表着我们能够通过栈或者队列来实现获取二叉树的遍历序列呢?如下所示:
中序遍历
演示中我们以中序遍历为例进行了递归算法和栈的执行过程,从演示的结果来看我们通过栈来实现中序遍历序列的获取是没有任何问题的,下面我们就可以尝试着通过栈来实现一下二叉树的中序遍历:

//中序遍历——栈
void InOrder2(BTN* root) {
	assert(root);
	LinkStack S;//创建链栈
	InitStack(&S);//初始化栈
	BTN* p = root;//遍历二叉树的指针
	while (p || !isEmpty(S)) {
		//当二叉树不为空树或者栈不为空栈时进入循环
		if (p != NULL) {
			Push(&S, p);//当树为非空树时,将根结点入栈
			p = p->lchild;//继续遍历左子树
		}
		else {
			Pop(&S, &p);//将栈顶元素出栈
			visit(p);//访问根结点
			p = p->rchild;//继续遍历右子树
		}
	}
}

该算法的逻辑并不复杂,下面我就来给大家介绍一下算法的具体思想:

  1. 在算法中通过可以存放二叉树的结点的栈来实现二叉树的中序遍历,栈对应的数据类型如下:
typedef struct LinkNode {
	BTN data;//数据域存放二叉树结点
	struct LinkNode* top;
}LinkStack;
  1. 算法中通过指向二叉树各个结点的临时指针p来完成二叉树的遍历;
  2. 递归的过程则非递归的方式来实现:
    • 当指针p为空指针且栈S为空栈时表示二叉树的所有结点都已遍历完,此时就可以跳出循环了;
    • 当指针p或者栈S有一方为非空的状态,则代表二叉树中还有结点没有遍历完,需要继续进行遍历;
  3. 在循环体内通过指针p的值来控制具体的操作内容:
    • 当指针p为非空指针时,需要先将指针p指向的结点进行入栈操作,随后开始继续遍历该结点的左子树,这一过程替代的是递归的向左子树递进的过程;
    • 当指针p为空指针时,此时栈S的栈顶元素为指针p此时指向的子树的根结点,所以需要将栈顶元素进行出栈,并让指针p指向该结点,这已过程替代的是左子树回归的过程;之后在访问p指向的结点中的元素;最后开始继续遍历该结点的右子树,这一过程替代的是递归的向右子树递进的过程;

相信大家现在应该理解了如何将中序遍历的递归算法转换为非递归算法了。下面我们就来分析一下为什么可以通过栈来实现递归算法与非递归算法之间的转换:

  • 首先我们需要理清递归与非递归转换的难点在哪?
    • 二叉树是一种递归型的树形结构,当我们通过二叉链表实现时,从上往下进行递进是没有问题的,这就好比单链表的遍历,我们只需要通过移动指向结点的指针即可完成;
    • 对于递归而言,因为我们有附加限制条件,当算法向下递进到满足限制条件时就会自动进行回归,而非递归算法无法做到自动回归,因为如果需要回归,那我们就是从下往上找父结点,所以如果要实现非递归的方式进行遍历那我们需要解决的第一件事就是如何找到每个结点的父结点
  • 其次我们需要理解为什么采用栈?
    • 之所以借助栈来实现,一是为了解决第一个问题——记录每个结点的父结点,二是在二叉树的中序遍历中,我们不难发现当我们从上往下遍历时,所记录的父结点的顺序与我们从下往上找父结点时的顺序刚好相反,例如从上往下记录的顺序为 a b c d e f g abcdefg abcdefg,在我们从下往上找父结点时的顺序则是 g f e d c b a gfedcba gfedcba,这刚好符合后入先出的操作特性,因此使用栈会更加贴合一点;
  • 最后我们需要明白栈的作用
    • 栈在整个算法中主要起到记录根结点的作用,当我们从上往下遍历时,入栈的元素都是对应子树的根结点,而当我们从下往上找时找的则是只遍历了左子树的子树对应的根结点,如下所示:

栈的作用
在上图中,此时以 a 7 a_7 a7为根的子树中的所有结点都已经完成了遍历,如果是递归算法的话,算法的过程是先回归到 a 7 a_7 a7这棵子树,再回归到 a 4 a_4 a4这棵子树,最后才能回归到我们需要进行还未完成遍历的 a 2 a_2 a2这棵子树;

这时我们不难发现,对于栈而言,此时的栈顶元素就是我们下一次需要访问的子树的根结点,因此栈除了记录结点外,还能实现快速跳转的作用,从已经完成遍历的子树跳转到还未完成遍历的子树。

因此对于前序遍历而言,它相比于中序遍历只需要在记录结点前完成结点的访问即可,这个比较简单我就不再展示对应的代码了。而对于后序遍历而言,它的非递归实现相对复杂一点,相比于前序和中序而言,它的根结点的访问是在左右子树之后,因此如果我们要访问根结点的话,我们就必须保证左右子树都完成了遍历,才能对其进行访问。如下所示:
后序遍历
可以看到,当我们要通过栈实现后序遍历时,同一个根结点我们是需要使用两次:

  • 当左子树完成遍历后进行回归时,需要通过栈顶元素完成右子树的遍历
  • 当右子树完成遍历后进行回归时,需要将栈顶元素出栈并完成访问

因此在后序遍历中,我们需要对右子树是否完成遍历作为栈顶元素出栈的依据。那如何来判断右子树是否完成访问则是我们在后续遍历的非递归实现中需要解决的一个问题。

当我们要通过非递归的方式实现后序遍历时,我们不妨在二叉树的结点中加入一个右子树遍历标志:

  • 标志的初始值为0,当未进行右子树的遍历时,标志为0;
  • 当开始进行右子树的遍历时,标志为1;

因此对应的数据类型应该改为:

//二叉树的数据类型
typedef int ElemType;
typedef struct BiTreeNode {
	ElemType data;//数据域
	struct BiTreeNode* lchild, * rchild;//指针域
	int right;//右子树遍历标志
}BTN, * BTL;
//BTN——二叉链表结点类型
//BTL——二叉链表类型

对应的代码实现如下所示:

//后序遍历——栈
void InOrder2(BTN* root) {
	assert(root);
	LinkStack S;//创建链栈
	InitStack(&S);//初始化栈
	BTN* p = root;//遍历二叉树的指针
	while (p || !isEmpty(S)) {
		//当二叉树不为空树或者栈不为空栈时进入循环
		if (p != NULL) {
			Push(&S, p);//当树为非空树时,将根结点入栈
			p = p->lchild;//继续遍历左子树
		}
		else {
			GetTop(S, &p);//当树为空树时,找到根结点
			//当结点的右子树标志为1时
			if (p->right) {
				Pop(&S, &p);//将栈顶元素出栈
				visit(p);//访问根结点
				GetTop(S, &p);//找到下一棵子树的根结点
				p->right = 1;//将右子树标志设为1
			}
			//结点的右子树标志为0时
			else {
				p->right = 1;//将右子树标志设为1
			}
			p = p->rchild;//继续遍历右子树
		}
	}
}

在增加了这个右子树标志之后,当遍历的结点为空时,我们就可以根据先找到该结点的父结点,再通过父结点的右子树标志进行下一步操作:

  • 当标志为1时,表示该结点的右子树完成了遍历,则对栈顶元素进行出栈,并访问该结点,之后还需要再一次访问此时的栈顶元素来找到下一棵遍历的子树的根结点并将其右子树标志设为1;
  • 当标志为0时,说明该结点目前是进行的左子树回归,并未进行右子树的遍历,因此只需要将根结点的右子树标志设为1;

这时可能有朋友会好奇,为什么当我们对栈顶元素进行出栈后再一次获取的栈顶元素我们都需要访问其右子树呢?

理解这个问题的关键就在于我们在对该结点入栈后所进行的操作内容是什么?有朋友很快就反应过来了,入栈后我们继续做的是访问该结点的左子树,因此当遇到结点为空时,表示已入栈的元素的左子树以完成了遍历,只剩右子树还未进行遍历。所以只要是获取了新的栈顶元素,那我们接下来肯定是要对其右子树进行遍历。

现在二叉树的递归算法和非递归算法的转换我们就介绍完了,这里我们是以结点序列为例来进行介绍的两种算法的转换,实际上从代码中我们也可以看出,我们在访问根结点时使用visit函数代替的,因此该转换的思路不一定是只用于结点序列的问题上,我们还可以拓展到其他问题上,这里我就不展开叙述了,如果后续有遇到相关的题目,我会再和大家进一步分享。

结语

在今天的内容中,我们详细介绍了二叉树的三种遍历方式以及C语言的递归实现:

  • 先序遍历(先根遍历):根结点—>左子树—>右子树
  • 中序遍历(中根遍历):左子树—>根结点—>右子树
  • 后序遍历(后根遍历):左子树—>右子树—>根结点

之后我们为了区分这三种遍历方式又介绍了对应的结点序列以及手动计算二叉树的结点序列的方式:

  1. 先找到根结点对应的子树的序列
  2. 通过遍历算法找到其左右子树对应的序列
  3. 合并子树序列

最后我们又探讨了一下在获取结点序列时这三种遍历方式的非递归实现。在递归算法转换到非递归算法时,我们需要解决的一个问题就是如何访问结点的父结点,当然解决的方式有很多,在今天的内容中我们主要是以栈来进行说明,朋友们你们自己也可以尝试通过队列、顺序表、链表等方式来实现。

今天的内容到这里就全部结束了,如果大家喜欢博主的内容,可以点赞、收藏加评论三连支持一下博主,当然也可以转发给身边需要的朋友。在下一篇内容中,咱们将会继续介绍二叉树的一些基本操作以及C语言实现,大家记得关注哦!最后感谢各位朋友的支持,咱们下一篇再见!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SAP 限制物料类型在BOM组件中简介

我们在创建BOM的时候通常是基于成品或者是半成品虚拟件创建BOM。正常情况下某些特殊的物料类型是不存在BOM中的。我们可以通过系统后台配置的方式对物料类型进行控制,控制对应的物料类型是否允许出现在BOM的组件中 1、后台配置路径: SPRO—生产—基本信息—物料清单—项目数…

【Linux取经路】网络套接字编程——TCP篇

文章目录 前言十、Tcp Server 端代码10.1 socket、bind10.1 listen——监听一个套接字10.2 accept——获取一个新连接10.3 read——从套接字中读取数据10.4 write——向套接字中进行写入10.5 Tcp Service 端完整代码(单进程版)10.6 Tcp Server 端代码&am…

【ZYNQ】CPU 私有定时器

Zynq 的每个 Cortex-A9 处理器都有自己的专用 32 位定时器和 32 位看门狗定时器,两个处理器共享一个全局 64 位定时器,这些计时器的时钟频率始终为 CPU 频率的 1/2。本文主要介绍 Zynq 芯片 CPU 私有定时器的工作特性,以及私有定时器的基本使…

(面试官问我微服务与naocs的使用我回答了如下,面试官让我回去等通知)微服务拆分与nacos的配置使用

微服务架构 正常的小项目就是所有的功能集成在一个模块中,这样代码之间不仅非常耦合,而且修改处理的时候也非常的麻烦,应对高并发时也不好处理,所以 我们可以使用微服务架构,对项目进行模块之间的拆分,每一…

精选网络安全书单:打造数字世界的钢铁长城!

目录 1.前言 2.书单推荐 2.1. 《内网渗透实战攻略》 2.2. 《Kali Linux高级渗透测试(原书第4版)》 2.3. 《CTF那些事儿》 2.4. 《权限提升技术:攻防实战与技巧》 2.5. 《数字政府网络安全合规性建设指南:密码应用与数据安全…

C++:红黑树

红黑树的概念 红黑树是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出两倍,因而是接近平衡…

ArrayList——简单洗牌算法

特殊语法介绍&#xff1a; List<List<E>> 该语法情况比较特殊&#xff0c;相当于一个“二维数组”存着一个个线性表的结构&#xff0c;如图&#xff1a; 该语法的灵活性强&#xff0c;可适用于多种类型和多种情况。接下来就使用该语法来实现一个简单的洗牌操作。…

halo进阶-主题插件使用

开始捣鼓捣鼓halo&#xff0c;换换主题&#xff0c;加个页面 可参考&#xff1a;Halo 文档 安装/更新主题 主题如同壁纸&#xff0c;萝卜青菜各有所爱&#xff0c;大家按需更换即可&#xff1b; Halo好在一键更换主题&#xff0c;炒鸡方便。 安装/更新插件 此插件还扩展了插件…

CR80通用清洁卡:证卡打印机、ATM机、POS机、读卡器等卡片设备清洁维护的好助手!

随着科技的进步&#xff0c;ATM机、POS终端、门禁系统、证卡打印机、读卡器等卡片设备在我们的日常生活中扮演着越来越重要的角色&#xff0c;些设备在长时间使用和环境因素的影响下&#xff0c;容易积聚油脂、灰尘和其他污染物&#xff0c;从而对其性能和功能产生负面影响。 深…

Xcode 打包报错Command PhaseScriptExecution failed with a nonzero exit code

解决办法: 1、在Xcode项目中 Pods -> Targets Support Files -> Pods-项目名 -> Pods-项目名-frameworks 中(大约在第44行) 加上 -f 2、CocoaPods版本太旧了,可以尝试升级CocoaPods版本 使用sudo gem update cocoapods更新cocoapods&#xff0c;问题将在1.12.1版本已…

力扣2968.执行操作使频率分数最大

力扣2968.执行操作使频率分数最大 方法一&#xff1a;滑窗 前缀和 求前缀和数组s 求一个数组补齐到中位数的差值 枚举右端点 class Solution {public:int maxFrequencyScore(vector<int>& nums, long long k) {int res0,n nums.size();sort(nums.begin(),nums…

单元测试AIR原则:提升代码质量的秘密武器

文章目录 引言一、AIR原则1. Automatic&#xff08;自动化&#xff09;2. Independent&#xff08;独立性&#xff09;3. Repeatable&#xff08;可重复性&#xff09; 二、Automatic&#xff08;自动化&#xff09;三、Independent&#xff08;独立性&#xff09;四、Repeatab…

GIF录屏工具Gif123 v3.3.0单文件

软件介绍 GIF的优势是小、轻、快&#xff0c;适合时间短、画面小、需要嵌入其他页面&#xff0c;打开就自动循环播放的动画。Gif123可录制合成鼠标轨迹,可调整鼠标指针大小,可在设置中打开鼠标指针高亮光圈功能,高亮光圈可跟随鼠标移动以指示鼠标位置。软件极其简单&#xff0…

C语言实现教学计划编制问题,Dev C++编译器下可运行(240606最新更新)

背景&#xff1a; 问题描述 大学的每个专业都要编制教学计划。假设任何专业都有固定的学习年限&#xff0c;每学年含两学期&#xff0c; 每学期的时间长度和学分上限都相等。每个专业开设的课程都是确定的&#xff0c;而且课程的开设时间的安排必须满足先修关系。每个课程的先…

树形表/树形数据接口的开发

数据表格式 需要返回的json格式 点击查看json数据 [{"childrenTreeNodes" : [{"childrenTreeNodes" : null,"id" : "1-1-1","isLeaf" : null,"isShow" : null,"label" : "HTML/CSS","na…

Spark MLlib 机器学习详解

目录 &#x1f349;引言 &#x1f349;Spark MLlib 简介 &#x1f348; 主要特点 &#x1f348;常见应用场景 &#x1f349;安装与配置 &#x1f349;数据处理与准备 &#x1f348;加载数据 &#x1f348;数据预处理 &#x1f349;分类模型 &#x1f348;逻辑回归 &a…

【成品设计】基于NB模块智能烟感系统设计

《基于NB模块智能烟感系统》 整体功能&#xff1a; 所需器件&#xff1a; 选用STM32F103为主控&#xff0c;选用DS18B20温度传感器和MQ烟雾传感器作为数据采集点&#xff0c; 采用0.96寸屏显实时显示采集到的温度、烟雾等数据&#xff0c;用蜂鸣器作为报警装置。 通过有人物联…

OpenCV的“画笔”功能

类似于画图软件的自由笔刷功能&#xff0c;当按住鼠标左键&#xff0c;在屏幕上画出连续的线条。 定义函数&#xff1a; import cv2 import numpy as np# 初始化参数 drawing False # 鼠标左键按下时为True ix, iy -1, -1 # 鼠标初始位置# 鼠标回调函数 def mouse_paint(…

初学者如何对大模型进行微调?

粗略地说&#xff0c;大模型训练有四个主要阶段&#xff1a;预训练、有监督微调、奖励建模、强化学习。 预训练消耗的时间占据了整个训练pipeline的99%&#xff0c;其他三个阶段是微调阶段&#xff0c;更多地遵循少量 GPU 和数小时或数天的路线。预训练对于算力和数据的要求非…

冯喜运:6.7今日黄金原油行情分析及独家操作策略

【黄金消息面分析】&#xff1a;周三&#xff08;6月5日&#xff09;&#xff0c;金价回升逾1.2%&#xff0c;收盘报每盎司2,355.49美元&#xff0c;全面收复前一交易日的跌幅。周三当天前公布的美国民间就业数据弱于预期&#xff0c;增强了美联储将在今年晚些时候降息的预期&a…