【数据结构】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.mzph.cn/diannao/23639.shtml

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

相关文章

1.6T模块与DSP技术的演进

近日,光通信行业市场机构LightCounting在市场报告中指出,去年的模块供应商已经展示了首批1.6T光学模块的风采,而今年,DSP供应商更是着眼于第二代1.6T模块设计的未来。这些前沿技术的突破,不仅代表了数据传输速度的飞跃…

三十九、openlayers官网示例Extent Interaction解析——在地图上绘制范围并获取数据

官网demo 地址: Extent Interaction 在openlayers中可以使用ExtentInteraction添加交互事件,配合shiftKeyOnly实现按住shift键绘制边界区域。 const map new Map({layers: [new TileLayer({source: new OSM(),}),],target: "map",view: new …

开源!过程控制与自动化系统

软件介绍 ProviewR是一个基于GPL许可的过程控制与自动化系统,最初由瑞典的Mandator和SSAB Oxelsund开发。作为一个成熟、集成且低成本的自动化解决方案,ProviewR在以Linux作为操作系统的标准PC上运行。该系统包含了顺序控制、调整、数据采集、通信、监控…

AI大模型语料库

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 语料库概述 语料库(Corpus)是一个存储了大量真实语言使用实例的集合,这些实例可以是文本、语音、视频等多种形式的语言数据。语料库通常…

ESP32 Error creating RestrictedPinnedToCore

随缘记,刚遇到,等以后就可能不想来写笔记了。 目前要使用到音频数据,所以去用ESP-ADF,但在使用例程上出现了这个API有问题,要去打补丁。 但是我打补丁的时候git bash里显示not apply,不能打上。 网上看到…

物联网开发tcp协议之——netty拆包问题

1.前言 tcp协议是物联网开发中比较常见的一种通信协议,而netty则是一tcp通信协议中一个比较优秀的框架。tcp协议是一种长连接的协议,是流式传输的,开发过程中最长遇见的问题就是拆包粘包问题。我目前对接过的物联网系列有智能家居设备&#…

神奇的python语法

今天发现一个神奇的语法规则 import numpy as np aa np.zeros((4,3)) print(aa) print(aa .shape[0] * aa. shape[1])结果是不报错的 结果是正常运行的 包括在jupyter里面,这种写法还是没有报错

氧兜:新一代隔热防晒膜,打造您的健康氧吧,开启品质生活新篇章

随着人们对生活品质追求的不断提升,氧兜品牌凭借创新科技,为您带来了新一代隔热防晒膜。它不仅具备卓越的隔热防晒功能,更能通过释放负氧离子,为您打造一个居家办公或出行环境的氧吧,让您的生活更加健康、舒适。 一、…

三十四篇:办公效率革命:深入探索办公自动化系统的全面策略

办公效率革命:深入探索办公自动化系统的全面策略 1. 引言 1.1 办公自动化系统(OAS)的定义与关键作用 在当前的企业环境中,办公自动化系统(Office Automation System, OAS)已成为提高效率和执行力的关键技…

全面守护你的健康ZL-0891A小动物多参数监护仪

简单介绍: 12.1英寸彩色TFT显示,分辨率800X600,采用数字血氧DSP算法,低灌注,小动物多参数监护仪具有优良的抗运动性能;动物用血压算法,支持测量各种动物类型,特有的中英文语音报警;支持USB数据导出,可以在…

Ubuntu部署开源网关Apache APISIX

说明 系统:Ubuntu 24.04 LTSDocker版本:v26.1.3Docker Compose版本:v2.26.1 下载和配置 Ubuntu需要安装Docker和Docker Compose 下载apisix-docker仓库 git clone https://github.com/apache/apisix-docker.git修改docker-compose 配置e…

C语言小例程8/100

题目&#xff1a;输出特殊图案&#xff0c;请在c环境中运行&#xff0c;看一看 程序分析&#xff1a;字符共有256个。不同字符&#xff0c;图形不一样。 #include<stdio.h> int main() {char a176,b219;printf("%c%c%c%c%c\n",b,a,a,a,b);printf("%c%c%c…

:长亭雷池社区版动态防护体验测评

序 长亭雷池在最近发布了动态防护功能&#xff0c;据说可以动态加密保护网页前端代码和阻止爬虫行为、阻止漏洞扫描行为等。今天就来体验测试一下 WAF 是什么 WAF 是 Web Application Firewall 的缩写&#xff0c;也被称为 Web 应用防火墙。区别于传统防火墙&#xff0c;WAF …

Vivado 设置关联使用第三方编辑器 Notepad++

目录 1.前言2.Vivado关联外部编辑器步骤3.Notepad的一些便捷操作 微信公众号获取更多FPGA相关源码&#xff1a; 1.前言 Vivado软件自带的编辑器超级难用&#xff0c;代码高亮对比不明显&#xff0c;而且白色背景看久了眼睛痛。为了写代码时有更加舒适的体验&#xff0c;可以…

LLama2源码分析——Rotary Position Embedding分析

参考&#xff1a;一文看懂 LLaMA 中的旋转式位置编码&#xff08;Rotary Position Embedding&#xff09; 原理推导参考自上文&#xff0c;以下结合huggingface代码分析公式计算过程 1 旋转角度计算 计算公式如下&#xff0c;其中d为词嵌入维度&#xff0c;这部分和论文原文…

STM32 音乐播放器之音频入门实验(pwm、dac、.wav、.mp3)

1.pwm实现简易电子琴实验 1.改变PWM频率&#xff0c;输出不同音调 2.改变占空比&#xff0c;调节音量大小 3.按键弹奏&#xff0c;支持按按键录取弹奏音 4.播放:中高低音&#xff1b;录取音&#xff1b;指定歌曲 5.支持按上一首&#xff0c;下一首&#xff0c;调弹奏速度&#…

Docker Desktop下载安装

Window下使用Docker推荐使用Docker Desktop; Docker Desktop是一款适用于Windows操作系统的桌面应用&#xff0c;它为开发人员提供了一个界面化操作Docker的环境&#xff0c;以便在本地环境中轻松创建、构建和运行Docker容器: Windows系统下Docker Desktop的安装 官网下载安装…

英国树莓派五大天王和你相约上海国际嵌入式展!

6月12日-14日 上海世博展览馆3号馆 H3馆 237展位 树莓派(Raspberry Pi),这个曾经让全球掀起"创客热潮"的小型单板电脑,如今已经成为嵌入式行业不可或缺的一员。作为行业先驱,树莓派基金会正携手团队,亮相2024年6月12日至6月14日在上海举办的 Embedded World上海国…

城镇污水处理设施运维服务认证

初次申请认证时需提交的文件/资料 1、通用文件/资料(证明文件复印件需签字盖公章) ☐ 营业执照复印件、统一社会信用代码/组织机构代码证复印件 ☐ 增值税一般纳税人资格证复印件&#xff0c;或其他增值税一般纳税人资格认定文件复印件 ☐ 资质 或 许可证 复印件&#x…

RT-Thread

RT-Thread RT-Thread 版权属于上海睿赛德电子科技有限公司&#xff0c;于 2006年 1月首次发布&#xff0c;初始版 本号为0.1.0&#xff0c;经过 10来年的发展&#xff0c;如今主版本号已经升级到3.0&#xff0c;累计开发者达到数百万&#xff0c; 在各行各业产品中装机量达到了…