【数据结构】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语言实现,大家记得关注哦!最后感谢各位朋友的支持,咱们下一篇再见!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文搞懂常见的数据拆分方案

常见的几种数据拆分方案 1、客户端分片 直接在应用层实现读取分片规则,解析规则,根据规则实现切分逻辑。 这种方案优缺点: 侵入业务(缺点); 实现简单,适合快速上线,容易定位问题; 对开发人员…

QT 信号和槽 一对多关联示例,一个信号,多个槽函数响应,一个信号源如何绑定多个槽函数

在窗体里放置一个单行文本编辑控件(QLineEdit)、一个标签控件(QLabel)和一个文本浏览控件(QTextBrowser),在单行文 本编辑控件里的文本被编辑时,标签控件和文本浏览控件都会同步显示…

Polar Web 【简单】- 被黑掉的站

Polar Web 【简单】- 被黑掉的站 Contents Polar Web 【简单】- 被黑掉的站思路EXP运行&总结 思路 如题目所述,这是一个被黑掉的站点,由此不禁要了解该黑客发现了哪些可以入手的路径,或是留下了什么样的文件供持续访问。 目录扫描该站点发…

前端工程化工具系列(九)—— mddir(v1.1.1):自动生成文件目录结构工具

mddir 是一个基于项目目录结构动态生成 Markdown 格式目录结构的工具,方便开发者在文档中展示文件和文件夹的组织结构。 1. 安装 全局安装改工具,方便用于各个项目。 pnpm i -g mddir2. 使用 在想要生成目录接口的项目内打开命令行工具,输…

【vue3+pinia+uniapp项目问题:使用pinia状态管理时store的数据更新,模板渲染视图不能实时更新】

在这里选择不同的学校后,发现store里面的数据打印出来能更新,但是使用store的数据打印出来并未实时更新且渲染在模板上,必须手动刷新视图才能更新。 原因是因为使用了解构赋值传入参数 解决方法 1.使用computed 现在视图能进行实时更新…

【机器学习】GLM4-9B-Chat大模型/GLM-4V-9B多模态大模型概述、原理及推理实战

​​​​​​​ 目录 一、引言 二、模型简介 2.1 GLM4-9B 模型概述 2.2 GLM4-9B 模型架构 三、模型推理 3.1 GLM4-9B-Chat 语言模型 3.1.1 model.generate 3.1.2 model.chat 3.2 GLM-4V-9B 多模态模型 3.2.1 多模态模型概述 3.2.2 多模态模型实践 四、总结 一、引言…

各平台对象存储

一、阿里云对象存储 官方文档:https://help.aliyun.com/zh/oss/getting-started/getting-started-with-oss?spma2c4g.11186623.0.0.299a646c6nWWcW 1.引入maven 官网:https://help.aliyun.com/zh/oss/developer-reference/java-installation?spma2c…

【代码随想录】【算法训练营】【第30天】 [322]重新安排行程 [51]N皇后 [37]解数独

前言 思路及算法思维,指路 代码随想录。 题目来自 LeetCode。 day 30,周四,好难,会不了一点~ 题目详情 [322] 重新安排行程 题目描述 322 重新安排行程 解题思路 前提:…… 思路:回溯。 重点&…

Android 开机动画的启动过程BootAnimation(基于Android10.0.0-r41)

文章目录 Android 开机动画的启动过程BootAnimation(基于Android10.0.0-r41)1.开机动画的启动过程概述2.为什么设置了属性之后就会播放? Android 开机动画的启动过程BootAnimation(基于Android10.0.0-r41) 1.开机动画的启动过程概述 下面就是BootAnimation的重要部…

Ubuntu系统中Apache Web服务器的配置与实战

✨✨ 欢迎大家来访Srlua的博文(づ ̄3 ̄)づ╭❤~✨✨ 🌟🌟 欢迎各位亲爱的读者,感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢,在这里我会分享我的知识和经验。&am…

【云岚到家】-day01-项目熟悉-查询区域服务开发

文章目录 1 云岚家政项目概述1.1 简介1.2 项目业务流程1.3 项目业务模块1.4 项目架构及技术栈1.5 学习后掌握能力 2 熟悉项目2.1 熟悉需求2.2 熟悉设计2.2.1 表结构2.2.2 熟悉工程结构2.2.3 jzo2o-foundations2.2.3.1 工程结构2.2.3.2 接口测试 3 开发区域服务模块3.1 流程分析…

shell的编程方式

文章目录 变量俩种方式第一种方式第二种方式 取消变量数组创建数组获取数组元素的方式 read输出的方式限制输入的方式 流程控制方式for循环输出的方式第一种方式第二种方式while循环输出的方式select选择输出的方式 判断方式判断的四种方式第一种方式第二种方式第三种方式 算术…

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

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

新手上路:Linux虚拟机创建与Hadoop集群配置指南①(未完)

一、基础阶段 Linux操作系统: 创建虚拟机 1.创建虚拟机 打开VM,点击文件,新建虚拟机,点击自定义,下一步 下一步 这里可以选择安装程序光盘映像文件,我选择稍后安装 选择linux系统 位置不选C盘,创建一个新的文件夹VM来放置虚拟机,将虚拟机名字改为master方便后续识别…

设计循环队列---力扣622

1、题目 1.1基础设置与讲解 循环队列,即固定长度的队列,可以想象成一个环形队列 就类似于这种队列,队尾指针后会有一个空位,用于控制判断队列为空还是为满; typedef int MyDataType;typedef struct {MyDataType fron…

2024百度之星 跑步

原题链接:码题集OJ-跑步 题目大意:一个n个人在绕圈跑,第i个人跑一圈的时间是i分钟,每二个人位置相同就会打一次招呼,如果同时来到终点,他们就会停下来,请问会打多少次招呼? 思路&a…

光猫、路由器的路由模式、桥接模式、拨号上网

下面提到的路由器都是家用路由器 一、联网条件 1.每台电脑、路由器、光猫想要上网,都必须有ip地址。 2.电脑获取ip 可以设置静态ip 或 向DHCP服务器(集成在路由器上) 请求ip 电话线上网时期,猫只负责模拟信号和数字信号的转换,电脑需要使…

【Pytorch】深入Pytorch模型的训练、log、可视化

文章目录 模型训练的模板综合案例-Pytorch 官网demo优化记录日志解析日志增加tensorboard数据记录保存训练曲线模型参数可视化增加wandb数据记录模型训练的模板 综合案例-Pytorch 官网demo pytorch 官网tutorial-quickstart https://blog.csdn.net/weixin_39107270/article/de…

【vue3|第6期】如何正确地更新和替换响应式对象reactive

日期:2024年6月5日 作者:Commas 签名:(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释:如果您觉得有所帮助,帮忙点个赞,也可以关注我,我们一起成长;如果有不对的地方&#xff…

【多模态】36、ShareGPT4V | 借助 GPT4V 的能够来生成更丰富的 caption 用于提升 LMM 模型的能力

文章目录 一、背景二、方法2.1 ShareGPT4V 数据集构建2.2 ShareGPT4V-PT 数据生成2.3 ShareGPT4V-7B Model 三、效果3.1 benchmark3.2 定量分析3.3 多模态对话 四、一些例子 论文:ShareGPT4V: Improving Large Multi-Modal Models with Better Captions 代码&#…