[入门必看]数据结构5.3:二叉树的遍历和线索二叉树

[入门必看]数据结构5.3:二叉树的遍历和线索二叉树

  • 第五章 树与二叉树
  • 5.3 二叉树的遍历和线索二叉树
    • 知识总览
      • 5.3.1_1 二叉树的先中后序遍历
      • 5.3.1_2 二叉树的层次遍历
      • 5.3.1_3 由遍历序列构造二叉树
      • 5.3.2_1 线索二叉树的概念
      • 5.3.2_2 二叉树的线索化
      • 5.3.2_3 在线索二叉树中找前驱后继
    • 5.3.1_1 二叉树的先中后序遍历
      • 什么是遍历
      • 二叉树的遍历
      • 二叉树的遍历(手算练习)
        • 练习1
        • 练习2
      • 先序遍历(代码)
      • 函数如何执行?
      • 求先序遍历序列
      • 求中序遍历序列
      • 求后序遍历序列
        • 练习
      • 例:求树的深度(应用)
    • 5.3.1_2 二叉树的层次遍历
      • 算法思想
      • 代码实现
    • 5.3.1_3 由遍历序列构造二叉树
      • 前序+中序遍历序列
        • Eg1:前序+中序遍历序列
        • Eg2:前序+中序遍历序列
      • 后序+中序遍历序列
        • Eg3:后序+中序遍历序列
      • 层序+中序遍历序列
        • Eg4:层序+中序遍历序列
        • Eg5:层序+中序遍历序列
      • 总结
      • 若前序、后序、层序序列两两组合
    • 5.3.2_1 线索二叉树的概念
      • 中序线索二叉树
      • 线索二叉树的存储结构
    • 5.3.2_2 二叉树的线索化
      • 中序线索化
      • 先序线索化
      • 后序线索化
    • 5.3.2_3 在线索二叉树中找前驱后继
      • 中序线索二叉树找中序后继
      • 中序线索二叉树找中序前驱
      • 先序线索二叉树找先序后继
      • 先序线索二叉树找先序前驱
      • 后序线索二叉树找后序前驱
      • 后序线索二叉树找后序后继
  • 知识回顾与重要考点
    • 5.3.1_1 二叉树的先中后序遍历
    • 5.3.1_2 二叉树的层次遍历
    • 5.3.1_3 由遍历序列构造二叉树
    • 5.3.2_1 线索二叉树的概念
    • 5.3.2_2 二叉树的线索化
    • 5.3.2_3 在线索二叉树中找前驱后继


第五章 树与二叉树

小题考频:30
大题考频:8


5.3 二叉树的遍历和线索二叉树

难度:☆☆☆☆

知识总览

5.3.1_1 二叉树的先中后序遍历

在这里插入图片描述

5.3.1_2 二叉树的层次遍历

5.3.1_3 由遍历序列构造二叉树

在这里插入图片描述

5.3.2_1 线索二叉树的概念

在这里插入图片描述

5.3.2_2 二叉树的线索化

在这里插入图片描述

5.3.2_3 在线索二叉树中找前驱后继

在这里插入图片描述


5.3.1_1 二叉树的先中后序遍历

在这里插入图片描述

什么是遍历

——遍历:按照某种次序把所有结点都访问一遍
在这里插入图片描述

线性结构:可以从头往后依次遍历,或者从尾开始往前面依次遍历。

在这里插入图片描述

树:层次遍历
——基于树的层次特性确定的次序规则

在这里插入图片描述

树:先/中/后序遍历
——根据二叉树的递归特性建立
本小节学习的三种遍历算法

二叉树的遍历

在这里插入图片描述
在这里插入图片描述

可以根据根、左子树、右子树三个部分被访问的顺序,指定三种遍历规则,分别是先序、中序和后序遍历。
如果子树还有下一级的结点,那么仍按照规则进行。

在这里插入图片描述

或者叫先根遍历、中根遍历、后根遍历。

Eg最简单的.
在这里插入图片描述

先序遍历:先访问根结点,根左右【ABC】
中序遍历:在中间访问根结点,左根右【BAC】
后序遍历:最后访问根结点,左右根【BCA】

Eg更复杂的.
在这里插入图片描述

先序遍历:根左右(ABC),先访问根结点【A】,因为左结点是一个分支结点,B子树也需要递归,所以需要对B子树也进行先序遍历【BDE】,然后对C子树先序遍历【CFG】,所以遍历顺序为:【A BDE CFG】
中序遍历:左根右(BAC),先访问B子树,也进行中序遍历【DBE】,然后访问根结点【A】,然后对C子树中序遍历【FCG】,所以遍历顺序为:【DBE A FCG】
后序遍历:左右根(BCD),先访问B子树,也进行后序遍历【DEB】,然后对C子树【FGC】,最后访问根结点【A】,所以遍历顺序为:【DEB FGC A

可以把叶子结点D理解为下面还连着两个空子树,所以对这样的一棵子树进行先序遍历、中序遍历、后序遍历得到的都只有D自己。


二叉树的遍历(手算练习)

练习1

在这里插入图片描述

  1. 先序遍历:
    在这里插入图片描述
  2. 中序遍历:
    在这里插入图片描述
  3. 后序遍历:
    在这里插入图片描述

这种手算确定序列的方法称为分支结点逐层展开法
如果一个结点是分支结点而不是叶子结点的话,就需要嵌套递归的按照特定的遍历序列把它展开到下一级

练习2

在这里插入图片描述

  1. 先序遍历:

- + /
- +a* /ef
- + a b- /ef
-+a
b-cd/ef

  1. 中序遍历:

+ - /
a+* - e/f
a+b*- - e/f
a+b*c-d-e/f

  1. 后序遍历:

+ / -
a*+ ef/ -
a b-* + ef/ -
abcd-*+ef/-

可以发现:
在这里插入图片描述

对算术表达式“分析树”进行前中后序遍历,可以得到算数表达式的前中后缀表达式。


先序遍历(代码)

在这里插入图片描述

定义结构体:
在这里插入图片描述
在这里插入图片描述
先序遍历:
在这里插入图片描述

  1. 如果二叉树非空,按根左右的顺序,先访问(visit)根结点(可以在visit函数中定义一些操作,比如说打印结点的值等等);
  2. 接下来先序遍历左子树,需要递归的调用先序遍历这个函数,传入左子树的根结点,即当前根的左孩子(lchild);
  3. 遍历完左子树后,先序遍历右子树,传入右孩子结点(rchild)

中序遍历和后序遍历的代码也及其类似:
中序遍历把visit函数放在中间;
后序遍历把vist函数放在最后。

中序遍历:
在这里插入图片描述
在这里插入图片描述

后序遍历:
在这里插入图片描述
在这里插入图片描述


函数如何执行?

在这里插入图片描述
在这里插入图片描述

——以先序遍历代码为例:
Step1:调用PreOrder函数,传入的参数T,指向结点A

由于结点非空,会vist访问这个结点,结点A是第一个被访问的结点

在这里插入图片描述

系统会记录下,当前执行到126行代码

Step2:递归左子树,函数会调用自身,传入的参数T,指向当前结点的左孩子(lchild),即结点B

由于结点非空,会vist访问这个结点,结点B是第二个被访问的结点

在这里插入图片描述

系统会记录下,当前执行到126行代码

Step3:递归左子树,函数会调用自身,传入的参数T,指向当前结点的左孩子(lchild),即结点D

Step4:接下来访问结点d的左孩子,为空,那么传入的参数T为NULL
在这里插入图片描述

因为不满足T != NULL,所以这一层的函数中什么也不做,直接return返回
这层函数执行结束后把信息弹出栈。

在这里插入图片描述

Step5:返回上一层的函数,继续处理结点D,之前执行的是126行,接下来执行127行,传入参数T变成了右孩子,即结点G
在这里插入图片描述

Step6:访问结点G,接下来访问G的左孩子(#126),为空,返回G结点;访问G的右孩子(#127),为空,返回G结点,且执行结束,返回D结点。
在这里插入图片描述

G的左孩子和右孩子都为空,所以#126和#127行函数什么也不会做,执行结束后返回D结点
在这里插入图片描述

在这里插入图片描述
Step7:D结点也执行结束,返回处理B结点这层函数。
在这里插入图片描述
Step?:接下来处理B的右子树(#127)…………
在这里插入图片描述

剩下的过程都是类似的,不再赘述。

可以看出这种递归实现遍历的算法,空间复杂度应该是O(h+1),其中h指的是二叉树的高度,+1是因为最后一层的叶子结点下面有两个空结点,处理空结点的这层函数也需要把信息压到栈顶。
 
舍去常数项:
时间复杂度为: O ( h ) O(h) O(h)


求先序遍历序列

在这里插入图片描述

【红色箭头】表示的是第一次路过这个结点;
【绿色箭头】表示的是第二次路过这个结点;
【紫色箭头】表示的是第三次路过这个结点;

在这里插入图片描述

从根结点出发,优先选择左边的路走,然后往右走,然后往上走。
先序遍历就是在第一次路过该结点的时候访问该结点【红色箭头】


求中序遍历序列

在这里插入图片描述
在这里插入图片描述

中序遍历就是在第二次路过该结点的时候访问该结点【绿色箭头】


求后序遍历序列

在这里插入图片描述
在这里插入图片描述

后序遍历就是在第三次路过该结点的时候访问该结点【绿色箭头】

以上,即“从你的全世界路过”法确定二叉树序列…

练习

再用这棵二叉树练练手:
在这里插入图片描述

补上空结点,然后开始模拟递归函数的执行原理来遍历
在这里插入图片描述

  • 先序遍历:- + a * b - c d / e f
  • 中序遍历:a + b * c - d - e / f
  • 后序遍历:a b c d - * + e f / -

例:求树的深度(应用)

在这里插入图片描述

根据二叉树的递归特性,分为根结点、左子树、右子树。

求一个树的深度,首先求出左子树的深度和右子树的深度,接下来对比左子树的深度和右子树的深度,选取深度更深的一边+1作为树的深度。
树的深度 = Max{左子树深度,右子树深度}+ 1

在这里插入图片描述

代码中,访问左 - 访问右 - 访问中,可以看做后序遍历的一个变种。


5.3.1_2 二叉树的层次遍历

在这里插入图片描述

该二叉树层序遍历结果为:A BC DEFG HIJKL

算法思想

  1. 初始化一个辅助队列

在这里插入图片描述
2. 根结点入队:

在这里插入图片描述
3. 若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话):

在这里插入图片描述

A出队,访问结点A,并将其左、右孩子B、C插入队尾

  1. 重复步骤3直至队列为空:

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

E结点没有左右孩子,访问完结点E之后没有结点入队

在这里插入图片描述在这里插入图片描述在这里插入图片描述

HIJKL结点没有左右孩子,访问完他们之后没有结点入队


代码实现

在这里插入图片描述

二叉树结点:
在这里插入图片描述

首先初始化一个辅助队列
在这里插入图片描述

链式队列结点数据结构定义
【使用链队列】:难以估计访问的树有多少个结点

在这里插入图片描述

初始化队列,让元素入队,在第三章中已经讲述过,不再赘述。

在这里插入图片描述
在这里插入图片描述

对于二叉树结点入队,并不需要在队列中保存一整个结点的真实数据,只需要保存这个结点的一个指针就可以了。

在这里插入图片描述

使用while循环:
在这里插入图片描述

当队列不空的时候,就会一直循环的让队头元素出队,然后访问此次出队的结点

在这里插入图片描述

可以在visit函数中自定义访问一个结点的时候需要进行的一系列操作,比如打印结点的值等

在这里插入图片描述

访问当前结点,如果有左孩子,将左孩子入队;如果有右孩子,将右孩子入队
一直循环,直到队列为空


5.3.1_3 由遍历序列构造二叉树

不同二叉树的中序遍历序列:
在这里插入图片描述

可以发现一个中序遍历序列可能对应多种二叉树形态

不同二叉树的前序遍历序列:

在这里插入图片描述
不同二叉树的后序遍历序列:

在这里插入图片描述
不同二叉树的层序遍历序列:
在这里插入图片描述

同样的,可以发现,如果只有前/中/后/层序遍历序列中的一种,那么其可能对应多种二叉树形态

结论:若只给出一棵二叉树的前/中/后/层序遍历序列中的一种,不能唯一确定一棵二叉树

在这里插入图片描述

一定要有中序遍历序列才能推出二叉树


前序+中序遍历序列

在这里插入图片描述

前序序列中最先出现的结点肯定是根结点,可以确定根结点在中序遍历中的位置
 
中序序列中根结点左边出现的结点一定是左子树中的结点,右边即右子树
 
如此递归地恢复出二叉树

Eg1:前序+中序遍历序列

在这里插入图片描述

在这里插入图片描述

前序序列中第一个出现A的一定是根结点

在这里插入图片描述

通过中序序列可以确定左子树中有三个结点BDC,右子树中有一个结点E
对应的前序序列中根结点后的三个结点DBC为左子树结点

在这里插入图片描述

相同的逻辑,前序序列中D是左子树的根结点;

在这里插入图片描述

中序序列中左边的B为左孩子,右边的C为右孩子

在这里插入图片描述


Eg2:前序+中序遍历序列

原理与Eg1.相同

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


后序+中序遍历序列

在这里插入图片描述

后序序列中最先出现的结点肯定是根结点,可以确定根结点在中序遍历中的位置
 
中序序列中根结点左边出现的结点一定是左子树中的结点,右边即右子树
 
如此递归地恢复出二叉树

Eg3:后序+中序遍历序列

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


层序+中序遍历序列

在这里插入图片描述

层序序列中第一个被访问的结点肯定是根结点,接下来访问的应该是左子树的根结点,下一个是右子树的根结点
 
思路类似,在中序序列找到根结点,划分左子树和右子树,然后找到左子树和右子树的根,进一步对左右子树进行下一层的划分
 
如此递归地恢复出二叉树

Eg4:层序+中序遍历序列

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

对于左子树来说,A就是根结点,左孩子是E,右孩子是F;对于右子树来说,B就是根结点,下一层左边应该是HC,右边应该是GI

在这里插入图片描述

下一层结点为CGHC的左孩子,IG的右孩子

在这里插入图片描述


Eg5:层序+中序遍历序列

在这里插入图片描述

从中序序列看出,A的左边是没有任何东西的,意味着这棵二叉树的左子树是空的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


总结

在这里插入图片描述

  • 最重要的是找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点

若前序、后序、层序序列两两组合

在这里插入图片描述

结论:前序、后序、层序序列的两两组合无法唯一确定一棵二叉树
一定要有中序遍历序列才能推出二叉树


5.3.2_1 线索二叉树的概念

在这里插入图片描述
之前已经了解过普通的二叉树,可以对它进行中序遍历:

在这里插入图片描述

从得到的遍历序列来看,他们之间其实是一种线性的关系;
每一个元素有一个与之对应的前驱,还有一个与之对应的后继
(和线性表一样,第一个元素没有前驱,最后一个元素没有后继)

一个结点只有一个唯一的前驱,并且可能会有多个后继,这是从树本身的逻辑结构出发定义的前驱和后继,与此处不同。

此处讨论的数据元素的前驱和后继是基于遍历序列来定义的前驱和后继。在这里插入图片描述

Q:能否从一个指定结点开始中序遍历?

每当对二叉树进行中序遍历,都需要从根结点出发,必须要知道根结点。

如果在某些场景中,只能知道某个结点的指针,并不知道根结点是哪个。
那么如果只给出G结点的指针,那么能否从G结点出发,对这棵树进行中序遍历呢?
——即遍历G B E A F C这些结点。

做不到!

因为G结点只有指向其孩子的指针,并不可能往回找到其双亲结点,或者找到其后继结点B。

  1. 普通的二叉树存在的第一个问题:
    每当对二叉树进行中序遍历,都需要从根结点出发,否则完成不了中序遍历。

在此处回忆线性表,如果这些元素D G B E A F C是一个线性表,知道其中任意一个元素的指针,就可以找到这个元素后面的所有元素。
也就是说:对线性表的遍历,并不是每一次都必须从头开始遍历,可以从任何一个元素开始往后遍历。
但普通的二叉树不可以,每一次遍历都必须从头开始。

  1. 普通的二叉树存在的第二个问题:
    如何找到指定结点p在中序遍历序列中的前驱和后继
    在这里插入图片描述

此处F结点是第6个被访问到的,其中序遍历中的前驱结点应该是A
如果只给出F结点的指针,肯定没办法找到前驱A,因为每一个结点只拥有向下一层的指针,不能往上找

思路:
从根结点出发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点
①当q==p时,pre为前驱
②当pre==p时,q为后继

在这里插入图片描述

中序遍历过程中,D结点是第一个被visit的结点,开始时让q指向D,此时没有前驱,所以pre是指向NULL

进行对比,qp并没有指向同一个结点,继续往后visit,在下一个结点被visit之前,把pre指针指向当前q所指向的结点:在这里插入图片描述
q指向下一个被visit的结点:
在这里插入图片描述
那么现在pre指针指向的结点就是q指针指向结点的,中序序列中的前驱

通过这个思路,让q指针不断指向后一个被访问的结点,然后pre指针依次后移,最终直到qp指向同一个结点
在这里插入图片描述

那么此时pre所指向的结点,就是p所指向结点的前驱

再后移一次,此时prep指向同一个结点
在这里插入图片描述

那么此时q所指向的结点,就是p所指向结点的后继

缺点十分明显:找前驱、后继很不方便;操作必须从头开始

如果在某些应用场景中,找前驱、找后继、遍历这些操作十分频繁的话,普通二叉树的设计会体现出缺点。


中序线索二叉树

在这里插入图片描述

对于一个有n个结点的二叉树,有n+1个空链域!可用来记录前驱、后继的信息。

比如G结点,他的前驱应该是D结点,所以可以让G的左孩子指针指向D,这样就可以通过G结点的左孩子指针来找到G结点的前驱

后继也是类似的,BG的后继,让G的右孩子指针指向B,这样就可以通过G结点的右孩子指针来找到G结点的后继

比如D结点,他是第一个被访问的结点,其没有前驱,那么可以让D的左孩子指针指向NULL,表示其没有前驱。

那么右图所示的二叉树,就是中序线索二叉树

如果一个结点的左右孩子指针指向的是前驱和后继,而不是左右孩子,那么这种类型的指针称为线索。
指向前驱的是前驱线索,指向后继的是后继线索。

那么思考这个Q:给定结点G的指针,如何找到G的后继?
A:通过后继线索来找到B

也就意味着从某一个结点出发,继续往后遍历(不断找后继)也是可行的。

又一个Q:那么对于一个右孩子指针指向其右孩子而不是后继,那么这个结点如何找到后继呢?
A:后面会探讨这个问题

本节只需理解线索二叉树,知道建立线索的意义是什么即可


线索二叉树的存储结构

在这里插入图片描述

线索二叉树中的左右孩子指针可能指向的不是左右孩子,而是前驱和后继。
为了区别这样两种状态,就需要增加两个标志位,分别叫ltagrtag

tag == 0时,表示这个指针指向孩子;
tag == 1时,表示这个指针指向“线索”。
左孩子指针充当的线索指向的是前驱;右孩子指针充当的线索指向的是后继。

二叉树可以称为二叉链表,那么线索二叉树可以称为线索链表

中序线索二叉树的存储可以这样表示:
在这里插入图片描述

原理类似,那么可以引申出先序线索二叉树和后序线索二叉树。

按先序/后序序列来确定各个结点之间的前驱和后继的关系,然后把n+1个空链域变成线索。

先序线索二叉树:
在这里插入图片描述
先序线索二叉树的存储:
在这里插入图片描述
后序线索二叉树:
在这里插入图片描述
后序线索二叉树的存储:

在这里插入图片描述
三种线索二叉树的对比:
在这里插入图片描述


5.3.2_2 二叉树的线索化

在这里插入图片描述
——用代码实现二叉树的线索化

用土办法找到中序前驱:
在这里插入图片描述

对于给定结点指针p,如何找到这个结点的前驱。
对整棵二叉树重新进行一次中序遍历,用q指针指向当前访问结点,用pre指针指向当前结点的前驱结点。
找中序遍历结点前驱的代码核心就是中序遍历;只要判断当前访问结点q和要找的结点p是否是同一个结点,如果是,pre指针指向的结点即p指针指向结点的前驱结点。
因此引入全局变量final来记录找到的前驱结点,这样就找到了它的中序前驱。

该函数最好换一个名字,比如findpre,可以很好地体现该函数的作用。

那么怎么对二叉树进行线索化呢?
将该结点的左孩子指针连上前驱/右孩子指针连上后继。
可以将上述思想迁移到二叉树的线索化中。


中序线索化

在这里插入图片描述

和普通二叉树相比,多了两个标志位——左、右线索标志

此处改变了函数名,但实际上该函数的内部就是一个中序遍历
在这里插入图片描述

左子树,访问该结点,右子树,中序遍历。

在这里插入图片描述

初步建成的二叉树ltag和rtag都设置为0

在这里插入图片描述

pre指针用来指向当前访问结点的前驱,第一个结点D没有前驱,让pre指针指向NULL

D结点是没有左孩子的,所以应该把他的左孩子线索化,在visit函数中完成判断:

在这里插入图片描述

如果当前结点左孩子为空,那么把其左孩子指针lchild指向他的前驱;
然后修改ltag = 1,因为值=1的时候才表示对应的孩子指针是线索。
然后让pre指针指向当前结点。

接下来访问G结点

同样的逻辑,其左孩子指针为空,让其左指针线索化指向pre,即前驱,
同时将对应的ltag设为1;

接下来访问B结点

B结点的左右孩子指针都是非空的,但是其前驱,也就是pre指针指向的结点,此时指向的结点G右孩子指针是空的,因此要将B结点的前驱pre指针指向的G结点的右孩子指针线索化,指向他的后继,即当前访问的q指针,其指向B结点。
 
pre的右孩子指针指向q,同时把rtag=1

最终完成线索化:
在这里插入图片描述

注意,当visit到第七个,也就是最后一个结点C的时候,最终还需要把pre指针指向当前这个结点C,此时出现了一个问题:
最后一个结点C的右孩子指针是空的,应该将其线索化。
此时设置的pre指针是一个全局变量,所以可以在其他的函数当中对
pre指针当前指向的结点再做处理,让其右孩子指针rchild指向NULL,表示没有后继,并将其rtag设置为1.

中序线索化的完整处理代码:
在这里插入图片描述

第一个被visit的结点,其没有前驱,所以pre的值初始化设置为NULL

如果二叉树非空,对其线索化,即调用函数InThread,本质也是一个中序遍历,只不过一边遍历一遍把各个结点进行线索化,具体线索化的逻辑在visit函数中进行。

在遍历处理完所有结点后,还需要处理最后一个结点,如果最后一个结点的右孩子指针是NULL,需要把rtag设置为1

中序线索二叉树:
在这里插入图片描述
另一版本中序线索化:
在这里插入图片描述

这种代码看不出与中序遍历之间的联系,是因为函数中多了一个参数pre。
此处的局部变量pre与之前方法中定义的全局变量pre的作用相同,用来记录当前被visit的根结点的前驱。
 
pre为以下函数中的局部变量:在这里插入图片描述
并作为参数传入中序线索化的函数中,

Q:为什么pre参数是引用类型?
A:为了保证在中序线索化函数中对pre进行修改可以影响到原始的pre的值。

Q:处理遍历的最后一个结点时,为什么没有判断rchild是否为NULL?
A:中序遍历的最后一个结点右孩子指针必为空


先序线索化

原理相同,本质上就是先序遍历,一边遍历,一边对结点进行线索化。
但是,先序线索化中会出现爱的魔力转圈圈问题
在这里插入图片描述

注意一个问题:假设当前正在visit第三个结点,pre指向第二个结点,那么按照visit函数中的逻辑,应该让第三个结点的左孩子指针线索化,即指向pre,同时让pre指针指向当前访问的这个结点。
但是在代码中,当处理完第三个结点D后,接下来会处理D的左子树,但是刚才把他的左孩子指针指向了结点B,接下来访问左子树时,会导致q指针再一次指回B对结点的访问会开始出现无限循环!

如何处理?
当左孩子指针线索化后,还有一个ltag变量,可以通过tag变量来判断左孩子指针指向的是否是真正的左孩子指针。
在这里插入图片描述

增加判断条件:ltag = 0,即lchild不是前驱线索时,才对其左指针所指向的子树进行线索化。

⚠️先序线索化中的易错点:
左孩子指针一旦被线索化之后,所指向的结点就是当前访问结点的前驱结点。
如果继续按照前驱线索访问,即又回头去访问其前驱结点,就会导致重复。

先序线索化的完整处理代码:
在这里插入图片描述

和中序线索化一样,处理最后一个结点,右孩子指针为NULL时,那么需要把rtag设为1

另一版本中序线索化:
在这里插入图片描述
在这里插入图片描述


后序线索化

也是一样的,区别在于先处理左子树,再处理右子树,最后处理这个根结点。
后序线索化中不会出现先序线索化的“转圈”问题,因为当访问当前结点时,当前结点的左子树和右子树肯定已经处理完了,不会出现“回头访问左子树”的情况。
在这里插入图片描述
另一个版本的后序线索化:
在这里插入图片描述
在这里插入图片描述

记得处理最后一个结点!


5.3.2_3 在线索二叉树中找前驱后继

在这里插入图片描述

中序线索二叉树找中序后继

怎么找一个指定结点p的中序后继?
在这里插入图片描述

① 指定结点p的右孩子指针已经被线索化,其rtag == 1,那么右孩子指针就是指向其中序后继,这种情况下找后继很方便。
② 指定结点p的右孩子指针没有被线索化,其rtag == 0,那么该结点是有右孩子的,即右子树非空。
 
中序遍历的顺序应该是左根右,访问p结点后,需要中序遍历右子树:
按照中序遍历的规则,p的右子树中,第一个被中序遍历的结点就应该是p的后继。
假设p的右孩子为叶子结点,没有下一层,那么该结点就是p的后继;
假设p的右孩子为分支结点,还有下一层,那么右子树中的左下角结点就是p的后继;
假设p的右孩子为分支结点,且左下角结点也为分支结点,那么这个分支结点的左孩子结点,即最左下角的结点就是p的后继

代码实现:
在这里插入图片描述

从指定结点出发,如果下一层还有左孩子,那么就一直顺着左分支找到最左下角的结点,即第一个被中序遍历的结点,即当前结点的后继结点。

可以利用找后继对中序线索二叉树进行中序遍历,传入要遍历的树的根结点指针T,首先利用Firstnode函数找到在这棵树中第一个被中序遍历的结点,即从根出发最左下角的这个结点,然后访问该结点,然后用刚才定义的Nextnode函数很方便地找到这个结点的中序后继。
 
用一个for循环,就可以实现对整棵中序线索二叉树进行中序遍历

该遍历算法不需要递归调用函数,所以空间复杂度为 O ( 1 ) O(1) O(1)


中序线索二叉树找中序前驱

在这里插入图片描述

① 指定结点p的左孩子指针已经被线索化,其ltag == 1,那么左孩子指针就是指向其中序前驱。
② 指定结点p的左孩子指针没有被线索化,其ltag == 0,那么该结点是有左孩子的。
类似后继,最右下角的结点就是p的前驱

代码实现:
在这里插入图片描述

通过Lastnode函数可以找到中序遍历最后一个被访问到的结点,即最右下角的结点。

Prenode函数中,如果p的ltag == 0即左指针没有被线索化,那么只需要找到p结点的左子树中最后一个被中序遍历的结点,即左子树中最右下角结点,该结点为p的前驱。
如果p的左指针本来就是线索的话,那么左指针指向的结点就是p的前驱。

可以利用找前驱对中序线索二叉树进行逆向的中序遍历


先序线索二叉树找先序后继

在这里插入图片描述
思路相同

① 右指针已经被线索化,直接返回后继线索。
② 右指针没有被线索化,那么肯定有右孩子的。
由于不知道有没有左孩子,先假设!
假设有左右孩子,按照先序遍历顺序为根左右,p结点的后继结点肯定是左子树中第一个被访问的结点,即左子树的根结点,就是他的左孩子。
假设只有右孩子没有左孩子,p结点的后继结点肯定是右子树中第一个被访问的结点,即右子树的根结点,就是他的右孩子。

②中
若p有左孩子,则先序后继为左孩子
若p没有左孩子,则先序后继为右孩子


先序线索二叉树找先序前驱

在这里插入图片描述
思路类似

探讨左孩子没有被线索化的情况:
ltag == 0时,p结点肯定有左孩子,按照先序遍历中根左右的顺序,p的左子树和右子树中的所有结点都只可能是p的后继,不可能在p的左右子树中找到前驱
线索二叉树只有指向孩子结点的指针,不可能往回找,除非从头开始遍历。

如何解决只有指向孩子结点的指针不能往回找的问题呢?

使用三叉链表
给各个结点设置一个指向其父结点的指针。

  1. 如果能找到p的父结点,且p是左孩子

在这里插入图片描述

按照遍历顺序 - 根左右,那么p结点肯定是在根结点 - 即其父结点之后被访问的一个结点,
那么这个父结点一定是p结点的先序前驱

  1. 如果能找到p的父结点,且p是右孩子,其左兄弟为空

在这里插入图片描述

按照遍历顺序 - 根右 ,那么p结点同样是在根结点 - 即其父结点之后被访问的一个结点,
那么这个父结点一定是p结点的先序前驱

  1. 如果能找到p的父结点,且p是右孩子,其左兄弟非空

在这里插入图片描述

按照遍历顺序 - 根左右 ,那么p结点的前驱就是在左兄弟子树中最后一个被前序遍历访问到的结点

  1. 如果p是根结点,则p没有先序前驱

以上基于改用三叉链表可以逆向找到p的父结点这个条件来讨论找先序前驱。


后序线索二叉树找后序前驱

在这里插入图片描述
思路相同

① 右指针已经被线索化,直接返回后继线索。
② 右指针没有被线索化,那么肯定有右孩子的。
由于不知道有没有右孩子,先假设!
假设有左右孩子,按照后序遍历顺序为左右根,p结点的前驱结点就是右子树中最后一个被访问的结点,也就是右孩子
假设没有右孩子,按照左根顺序,p结点的前驱结点就是左子树中最后一个被访问的结点,也就是“左孩子”。

②中
若p有右孩子,则后序前驱为右孩子
若p没有右孩子,则后序前驱为左孩子


后序线索二叉树找后序后继

在这里插入图片描述
思路类似

探讨右孩子没有被线索化的情况:
ltag == 0时,p结点肯定有右孩子,按照后序遍历中左右根的顺序,p的左子树和右子树中的所有结点都只可能是p的前驱,不可能在p的左右子树中找到后继
除非从头开始遍历。

使用三叉链表
给各个结点设置一个指向其父结点的指针。

  1. 如果能找到p的父结点,且p是右孩子

在这里插入图片描述

按照遍历顺序 - 左右根,那么无论p结点下面还有没有孩子,p结点一定是该右子树中最后一个被访问的结点,在访问完p结点之后,就会访问p结点的父结点,所以p结点的后序后继为其父结点

  1. 如果能找到p的父结点,且p是左孩子,其右兄弟为空

在这里插入图片描述

按照遍历顺序 - 左根 ,在访问完p结点后一定紧接着访问其父结点,
那么这个父结点一定是p结点的先序前驱

  1. 如果能找到p的父结点,且p是右孩子,其左兄弟非空

在这里插入图片描述

按照遍历顺序 - 左右根 ,那么p结点的后继就是在右兄弟子树中第一个被后序遍历访问到的结点

  1. 如果p是根结点,则p没有后序后继

以上基于改用三叉链表可以逆向找到p的父结点这个条件来讨论找后序后继。


知识回顾与重要考点

5.3.1_1 二叉树的先中后序遍历

在这里插入图片描述

  • 掌握先、中、后序遍历算法
  • 掌握求遍历序列的方法

5.3.1_2 二叉树的层次遍历

在这里插入图片描述

  • 二叉树的层次遍历,关键在于设置辅助队列

5.3.1_3 由遍历序列构造二叉树

在这里插入图片描述

  • 最重要的是找到树的根结点,并根据中序序列划分左右子树,再找到左右子树根结点

Q:若前序、后序、层序序列两两组合?
在这里插入图片描述

结论:前序、后序、层序序列的两两组合无法唯一确定一棵二叉树


5.3.2_1 线索二叉树的概念

在这里插入图片描述

  • 利用二叉树本身存在的n+1个空链域,将其变成线索并指向前驱、后继
  • 再普通二叉树的基础上,增加ltagrtag两个标志位,当tag值为1,指针指向“线索”,当tag值为0,指针指向孩子。
  • 中序前驱/中序后继:按照中序遍历二叉树后,该结点对应的前驱/后继。其他类似。
  • 以手算的方式线索化:根据遍历规则确定各个结点被访问的顺序并编号,将n+1个空链域连上前驱、后继。

5.3.2_2 二叉树的线索化

在这里插入图片描述

  • 增加指针pre用来记录当前访问结点的前驱结点,可以把pre设置为全局变量
  • 注意处理最后一个结点的rchild、rtag
  • 注意先序线索化中,“转圈”问题,只有当ltag == 0时,才能对左子树先序线索化

5.3.2_3 在线索二叉树中找前驱后继

在这里插入图片描述

  • 不需要背,理解过程,学会推算。解无定法,源于观察。
    在这里插入图片描述
  • 中序线索二叉树既可以找前驱也可以找后继
  • 先序线索二叉树可以找后继,不能找前驱;后续线索二叉树可以找前驱,不能找后继。除非从头遍历或者用三叉链表。
    在这里插入图片描述

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

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

相关文章

Linux:在VMware中,如果虚拟机之前可以上网,之后突然不能上网,怎么办?

Linux系统版本:centos 7.5 x64位 VMware版本: VMware Workstation Pro 16 文章目录 前言一、什么原因会导致这种问题并如何解决它?原因①:虚拟机没有启动网络服务原因②:外部主机上VMware的【VMware NAT Service】服务…

MySQL创建索引时提示“Specified key was too long; max key length is 767 bytes”

MySQL创建索引时提示“Specified key was too long; max key length is 767 bytes” 问题描述 数据库RDS MySQL版在创建表索引时,出现如下错误信息。 Error 1071: Specified key was too long; max key length is 767 bytes.ERROR 1709 (HY000): Index column siz…

【Java】Java中线程安全有哪些实现思路?

文章目录 1、使用 synchronized 关键字2、使用 ReentrantLock 类3、使用 ConcurrentHashMap 类4、使用 Atomic 类5、使用 ThreadLocal 类总结 在 Java 多线程编程中,线程安全是一个非常重要的概念。 线程安全通常指程序在多线程并发执行时,仍然能够保持正…

ANR实战案例 - FCM拉活启动优化

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 例如:第一章 Python 机器学习入门之pandas的使用 文章目录 系列文章目录前言一、Trace日志分析二、业务分析1.Firebase源码分析2.Firebase官方查看官方文档Dem…

Python学习26:个人所得税计算器

描述‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬ 2018年10月1日以前&#xff…

Elasticsearch:使用 count API 来获得所有文档的个数

在我开始使用 Elasticsearch 的时候,我希望获得给定查询的文档总数。比如我们想对数据进行分页显示。从 Elasticsearch 7.0之后,为了提高搜索的性能,在 hits 字段中返回的文档数有时不是最精确的数值。Elasticsearch 限制了最多的数值为10000…

截面空间计量模型(Stata)

截面空间计量模型(Stata) 文章目录 截面空间计量模型(Stata)[toc]1 广义空间自回归模型(SAC)2 空间误差模型(SEM)3 空间杜宾模型(SDM)4 广义空间嵌套模型(GNS)5 空间(自回归)滞后模型(SAR,SLM)6 空间杜宾误差模型(SDEM) 1 广义空间自回归模型&#xff08…

[ChatGPT] 从 GPT-3.5 到 GPT-5 的进化之路 | ChatGPT和程序员 : 协作 or 取代

⭐作者介绍:大二本科网络工程专业在读,持续学习Java,努力输出优质文章 ⭐作者主页:逐梦苍穹 ⭐如果觉得文章写的不错,欢迎点个关注一键三连😉有写的不好的地方也欢迎指正,一同进步😁…

考虑多能负荷不确定性的区域综合能源系统鲁棒规划(Python代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…

Linux 虚拟机 磁盘扩容

概述 在单台虚拟机上部署了过多服务,导致磁盘使用过度达到98%。 现在扩容提高磁盘容量,增加10G。 现象 df -h df -ih du -s具体步骤 VMware 扩容 关闭虚拟机的情况下执行,类似于生产环境下需要关闭服务器,从而添加硬盘等相关操作…

sed命令的应用

sed命令的应用 一、sed编辑器sed的工作流程:sed的命令格式于常用选项命令格式常用选项常用操作: 三、实际操作打印内容删除行替换行数内容插入内容字符位置互换 一、sed编辑器 sed是一种流编辑器,流编辑器会在编辑器处理数据之前基于预先提供…

BlockChain-Account_TakeOver

题目描述 ECDSA 签名 假设我们的私钥为 d A d_A dA​而公钥为 Q A Q_A QA​, Q A d A ⋅ G Q_Ad_A\cdot G QA​dA​⋅G,接下来就是签名的过程,要签名的消息为 m m m 取 e H A S H ( m ) e HASH(m) eHASH(m)取 e e e的左边的 L n L_n L…

Ambari-2.7.7源码编译

0 说明 本文基于Ambari-2.7.7版本进行源码编译。所需的编译资料统一提供如下: 链接:https://pan.baidu.com/s/1F2D7zBGfKihxTBArnOilTw 提取码:8m17 1 前提条件 1.1 下载ambari源码包 wget https://github.com/apache/ambari/releases/t…

Linux:文本三剑客之sed编辑器

Linux:sed编辑器 一、sed1.1 sed编辑器1.2 sed编辑器的工作流程1.3 命令格式1.4常用选项1.5 常用操作1.6 实际应用 一、sed 1.1 sed编辑器 sed是一种流编辑器,流编辑器会在编辑器处理数据之前基于预先提供的一组规则来编辑数据流。sed编辑器可以根据命…

FE_Vue学习笔记 常用指令的学习【v-model filters v-text v-html v-cloak v-once v-pre 自定义指令】

1 收集表单数据 v-model 收集表单数据&#xff1a; 若&#xff1a;<input type"text">&#xff0c;则v-model收集的是value的值&#xff0c;用户输入的就是value值。 若&#xff1a;<input type"radio">&#xff0c;则v-modle收集的是value的…

浏览csdn博客自动隐藏侧边栏并只看目录

背景 CSDN 总算做了点好事&#xff0c;能够隐藏大部分无关信息&#xff0c;只看博客内容本身。具体如图&#xff0c;还在测试版 以我的一篇博客为例&#xff0c;原始界面&#xff0c;花里胡哨一堆 点击隐藏侧栏后的清爽版 点击只看目录后的清爽版 前提提要 安装油猴脚本&…

OLS样本估计量抽样分布模拟

OLS样本估计量抽样分布模拟 文章目录 OLS样本估计量抽样分布模拟1 OLS估计量分布2 R语言实现 1 OLS估计量分布 对于线性回归方程 Y β 0 β 1 X ε Y \beta_0\beta_1 X \varepsilon Yβ0​β1​Xε 利用普通最小二乘法(OLS&#xff09;估计上述方程参数使的假定(之一)是…

[译] Flutter 3.10 的新功能

[译] Flutter 3.10 的新功能 原文 https://medium.com/flutter/whats-new-in-flutter-3-10-b21db2c38c73 无缝的Web和移动端集成&#xff0c;Impeller稳定版的突破性图形性能&#xff0c;以及更多 欢迎使用Flutter 3.10&#xff01;我们非常期待展示我们令人惊叹的Flutter社区所…

示波器的数据处理怎么记录?

示波器的使用 - 记录和保存示波器测试结果 安泰测试为您分享如何记录示波器的数据。 "从您把示波器探头连接到器件的那一刻起&#xff0c;信号就开启了一次瞬间即可完成的重大旅程。它必须 跨过五个不同的“模块”&#xff0c;才能完成从器件到示波器&#xff0c;最后返回…

十五、Gateway网关

目录 Zuul网关和gateway网关的区别&#xff1a; Gateway路由配置 1、新建服务网关项目&#xff0c;并在项目pom文件中引入gateway网关依赖 2、在application.yml配置gateway 3、如果不用配置的方式配置gateway路由&#xff0c;还可以通过代码的形式配置 4、启动网关服务&…
最新文章