当前位置: 首页 > news >正文

《算法与数据结构》第六章[第3节]:二叉树(第二部分)

一、线索二叉树

1、线索二叉树定义

  我们现在来回头看看之前实现的二叉链表的存储情况,如下图

图1:二叉链表存储情况

图1:二叉链表存储情况

  可以看出,还是有许多指针域是“^”,特别是叶子结点,它们的两个指针域都是空的,首先我们来看一看这种空指针域有多少。

  对于一棵有nnn个结点的二叉链表,每个结点都有lchildrchild两个指针域,所以这个二叉链表中有2n2n2n个指针域,而nnn个结点的二叉树一共有n−1n-1n1条分支线数,也就是说,存在的空指针域有2n−(n−1)=n+12n-(n-1)=n+12n(n1)=n+1个空指针域。比如图中有10个结点,而空指针域有11个,这些空间就是被白白浪费了。

  另一方面,我们来看图1中这棵树的中序遍历序列GDHBEIAFJCGDHBEIAFJCGDHBEIAFJC,虽然我们之前说树是一种一对多结构,没有明确的前驱后继关系,但是在遍历序列中,我们清楚知道“AAA”的前驱是“III”,后继是“FFF”,它们之间就有了明确的前驱后继关系。

  可是这样明确的的前驱后继关系是建立在已经遍历过后的基础上的,若未遍历过,我们就不知道它们之间这样的关系,若我们想再次获得,便需要再次遍历才能知道,那我们可不可以在建立二叉树的同时,根据类似某一规律(前中后序)建立起前驱后继关系呢?

  这样一来,我们同时可以把那些空指针域给利用起来,还能减少获取前驱后继的时间,一石二鸟,何乐不为。

  我们把这种指向前驱后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称作线索二叉树。

  我们将如图1的二叉树进行中序遍历后,就可以将所有空指针域中的rchild改为指向其后继结点,如下图所示

图2:线索二叉树(添加后继)

图2:线索二叉树(添加后继)

  这样我们就可以直接得到结点“GGG”的后继为“DDD”(①),结点“HHH”的后继为“BBB”(③)…而不用老老实实进行中序遍历后再先进入其不存在的孩子结点中再一层层返回了。而最后一个被遍历的结点“CCC”则指向空(NULL)作为结束。此时我们将5个指针域利用起来了。

  同样的道理,我们将没有利用起来的左孩子域lchild,改为当前结点的前驱,如下图。这样就有结点“GGG”的前驱为NULL(①),结点“HHH”的前驱为“DDD”(②),结点“EEE”的前驱为“BBB”(③)…这样又有6个空指针域被利用起来,与后继加起来正好是11个。

图3:线索二叉树(添加前驱)

图3:线索二叉树(添加前驱)

  此时我们保留图中原来的中序“双亲-孩子”访问联系,添加我们后序添加的前驱后继关系,将结点简化为圆点,再来看一看我们这棵树。

图4:简化线索二叉树

图4:简化线索二叉树

  细看可以看出,二叉树的线索化,实际上就是将它转化为双向链表,这样一来,我们就可以更加方便的对其进行增删改查操作了。

  我们对二叉树以某种次序遍历使其变为线索二叉树的过程称作线索化

  但是细心的同学会在上述过程中发现一个缺点,我们无法得知每个结点的lchild中存储的是这个结点的左孩子还是前驱,rchild也是同理,同时还存在部分结点lchild存储左孩子,rchild存储其后继结点,所以为此做出区分也是十分有必要的。因此,我们为每个结点再设置两个布尔型标志域ltagrtag来区分它们到底存储的是孩子还是前驱后继。这样一来,结点结构就变成了下图所示的样子。

图5:线索二叉树结点

图5:线索二叉树结点

  其中:

  • ltag为0(false)时指向该结点左孩子,为1(true)时指向其前驱;
  • rtag为0(false)时指向该结点右孩子,为1(true)时指向其前驱;

  因此图1中的二叉树存储情况就变成了如下所示(为避免前驱后继连线影响区分,省去了前驱后继连线)

图6:二叉链表线索化后存储情况

图6:二叉链表线索化后存储情况

2、线索二叉树结构实现

  根据上述内容,我们可以给出二叉树的线索存储结构定义代码如下:

typedef char ElemType;
typedef enum {Link, Thread} PointerTag;
// 使用枚举类型区分tag,Link表示指向孩子,Thread表示指向前驱后继typedef struct BiThrNode {ElemType data;						//结点数据域struct BiThrNode *lchild, *rchild;	//指针域PointerTag ltag;					//左标签域PointerTag rtag;					//右标签域
}BiThrNode, *BiThrTree;

  我们已经知道线索化就是将空指针域改为前驱后继的过程,并且我们只能在遍历后得知每个结点之间的前驱后继关系,所以我们就在遍历中修改空指针实现线索化。

  我们在此给出中序遍历线索化的过程代码:

BiThrTree pre;		//全局变量,用于指出刚才访问过的结点void InThreading(BiThrTree T)
{if(T){InThreading(T->lchild);	//对当前结点左子树进行线索化if(!T->lchild)			//没有左孩子{T->ltag=Thread;		//置左标签域为前驱T->ltag=pre;		//让左孩子域指向上一个访问结点}if(!pre->rchild)		//前驱没有右孩子{pre->rtag=Thread;	//置前驱的右标签域为后继pre->rchild=T;		//让前驱的右孩子域指向当前结点}pre=T;					//将当前结点作为上一访问结点进行后序访问InThreading(T->rchild);	//对当前结点右子树进行线索化}
}

  可以看到,大体上的中序规则是没有变的,只不过将中间的输出改为了线索化操作,我们来看看都干了些什么。

  首先是if(!T->lchild)表示当前结点的左孩子域为空,既然它为空我们就要让其指向其前驱,正好,全局变量pre存储了我们刚刚访问过的结点,我们只需将pre赋值给lchild后设置标签值即可完成其前驱结点的线索化。

  后继的判断变成了pre->rchild而不是T->rchild,这是为什么呢?因为我们还没有访问到当前结点的后继,哪怕当前结点的右孩子域是空的,我们目前也没有办法进行线索化,但是我们当前访问的结点是上一个访问结点(pre)的后继啊,那就意味着我们现在可以找到我们上一个访问结点的后继,若上一访问结点的右孩子域是空的,我们就可以直接将当前结点赋值给它的右孩子域并设置标签值了。也就是说,这里完成的后继线索化实际上是上一个访问的结点的。怎么样,是不是十分巧妙。

  当然,最后别忘了将当前结点置为上一访问结点再进行后序操作。

二、树、森林与二叉树的转换

  我们在前面已经探讨过树的定义和存储结构了,它的优势就是在满足树的条件下可以是任何形状,但是劣势也在这里,任意形状导致我们对树进行处理的各类操作十分复杂。我们在后面又学习了二叉树,相比与一般的树来说,二叉树因为结构固定,便衍生出了许多性质,而这些性质也极大地方便了我们对其所进行的许多操作。

  那既然二叉树这么好,我们就想办法把一般的树转换为二叉树呗?可行吗?当然可行,回忆一下我们在树的最后提到的孩子兄弟表示法,就是将树转换为二叉树来存储的一种途径。

1、树转换为二叉树

  将树转换为二叉树的步骤如下:

  1. 加线:在所有兄弟结点之间添加连线;
  2. 去线:对树中每个结点,只保留它与它的第一个孩子间的连线,删除它与其它孩子之间的连线;
  3. 层次调整:以树的根结点为轴,将整棵树旋转一定角度,使之结构层次分明。

  以下是示意图:

图7:树转化为二叉树

图7:树转化为二叉树

  可能有大部分同学喜欢这个方法,本人不是很习惯用这样的方法,所以下面再给出本人的方法。

  其实我的方法就是树的孩子兄弟表示法,只是将其简化后成了:从根结点开始,每个结点的最左孩子为其左孩子,这个左孩子的兄弟全部都在它(这个左孩子)的右子树分支上。

2、森林转换为二叉树

  森林是由若干棵树组成的,所以可以理解为,森林中每棵树都是兄弟,这样一来,我们就可以以兄弟关系来处理每棵树了。

  步骤如下:

  1. 将每棵树都转化为二叉树;
  2. 第一棵二叉树不变,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,并连线。将所有二叉树连接起来后就得到了由森林转化来的二叉树。

  以下为示意图:

图8:森林转化为二叉树

图8:森林转化为二叉树

3、二叉树转换为树

  我们可以看出,当将一棵树转换为二叉树时,二叉树根结点是不会有右子树的,但是将一个森林转换为二叉树时,这时的二叉树根结点就有右子树了。这也正是我们逆向转换的一个判定标准。即若一棵二叉树没有右子树,则它可以转换为一棵树;若一棵二叉树有右子树,则它可以转换为森林。

  我们已经学习了树转换为二叉树,那将二叉树转换回树便是将之前的操作还原一遍即可,操作步骤如下:

  1. 加线:若二叉树中的某一结点存在左孩子,则将其左孩子的右孩子,左孩子的右孩子的右孩子…均添加与其的连线;
  2. 去线:删去每一结点原来与其右孩子的连线;
  3. 层次调整:以树的根结点为轴,将整棵树旋转一定角度,使之结构层次分明。

  以下为示意图:

图9:二叉树转换为树

图9:二叉树转换为树

4、二叉树转换为森林

  同样,我们也已经学习了森林转化为二叉树的过程,且也已经知道若一棵二叉树其根结点存在右子树,则可将其转化为一个森林,步骤如下:

  1. 从根结点开始,若其右孩子存在,则去掉根结点与其右孩子的连线,分离出一棵二叉树;再看分离出来的二叉树,若其根结点右子树存在,则去掉其根结点与其右孩子的连线,重复上述操作,直到最后分离出的二叉树根结点没有右孩子为止;
  2. 再将每棵分离后的二叉树按照“二叉树转换为树”的方法转化为树即可。

  示意如下:

图10:二叉树转换为森林

图10:二叉树转换为森林

5、树与森林的遍历

  最后我们简单讲一下树与森林的遍历。

  树的遍历方式有两种。

  1. 先根遍历:先访问树的根结点,然后依次遍历根的每棵子树,类似于二叉树的先序遍历,只是树中的子树数量没有特别限制;
  2. 后根遍历:先依次以后根次序遍历树的每棵子树,然后再访问根结点,同样类似于二叉树的后序遍历。

  例如图9中的树④

图11:一棵树

图11:一棵树

  该树的先根遍历序列是“ABEFCDGABEFCDGABEFCDG”,后根遍历序列是“EFBCGDAEFBCGDAEFBCGDA”。

  具体的算法不在此提供,大家可以基于我们上述实现的几种树的存储结构来自行实现。

  森林的遍历方式也有两种。

  1. 先序遍历:先访问森林中第一棵树的根结点,然后依次先序遍历根的每棵子树,然后再依次以同样的方式遍历除去第一棵树的剩余树所构成的森林。
  2. 中序遍历:先访问森林中的第一棵树,以后根遍历的方式遍历该树,然后访问该树根结点,再依次以同样的方式遍历除去第一棵树的剩余树所构成的森林。

  关于森林的第二种遍历方式存在争议,严教授书中所写的是“中序遍历”,而程老师所写《大话数据结构》中将其称为“后序遍历”,网络上也是众说纷纭,其实两种名字都各有其原因:称其“中序遍历”是因为森林中每棵树的根对于整个森林的遍历序列来说不算是完全在最后,而且该方式遍历森林所得结果与将该森林转换为二叉树后的中序遍历结果是一致的,故称其中序遍历也是自然而然;而称其“后序遍历”多半是因为森林中每棵树都是以后根遍历的顺序来进行的,类似二叉树后序遍历,这样看来,称其后序遍历好像也无可厚非。但是在学习的过程中,认识其过程和思想远比知道其名字重要,大家完全没有必要为一个名字纠结。

  例如图10-③所示的森林,其先序遍历序列“ABCDEFGHJIABCDEFGHJIABCDEFGHJI”,中序遍历序列“BCDAFEJHIGBCDAFEJHIGBCDAFEJHIG”。这时我们就可以看到,若对图10-①中的二叉树进行先序遍历和中序遍历也会分别得到这两个序列。由此可见,不论是树还是森林,它们总是可以通过某种方式与二叉树/二叉链表产生联系,我们便可以通过这种联系结合二叉树的性质来简化问题了。


文章转载自:

http://08Xq1eNM.Lnbcg.cn
http://bJQv4PU7.Lnbcg.cn
http://Az7E3J8O.Lnbcg.cn
http://OTOyuTsl.Lnbcg.cn
http://4bvXmYNR.Lnbcg.cn
http://hh38Cq5r.Lnbcg.cn
http://ECjz0BNz.Lnbcg.cn
http://uyXjK6nN.Lnbcg.cn
http://360eR0wf.Lnbcg.cn
http://Rnt4piMH.Lnbcg.cn
http://YgzD6lf3.Lnbcg.cn
http://W79VApbU.Lnbcg.cn
http://NeqhVnWq.Lnbcg.cn
http://L7ay8fWt.Lnbcg.cn
http://H23k1m8t.Lnbcg.cn
http://qOtkj6Yr.Lnbcg.cn
http://d1qaMc9j.Lnbcg.cn
http://ephRzvbL.Lnbcg.cn
http://Inq57kcO.Lnbcg.cn
http://ABVom2ts.Lnbcg.cn
http://RxgpfBc4.Lnbcg.cn
http://yf2IwvyQ.Lnbcg.cn
http://wvk5Nsb9.Lnbcg.cn
http://CLLeNPDg.Lnbcg.cn
http://E32X8hiF.Lnbcg.cn
http://ZpbcQu5g.Lnbcg.cn
http://KpP5LK2e.Lnbcg.cn
http://z4H0ixfU.Lnbcg.cn
http://ZBLpQeoO.Lnbcg.cn
http://2dHExm9W.Lnbcg.cn
http://www.dtcms.com/a/381579.html

相关文章:

  • 深入理解 Python 中的 `__call__` 方法
  • AI 智能体的定义与演进
  • 鸿蒙Next ArkWeb网页交互管理:从基础到高级实战
  • 给CentOS的虚拟机扩容
  • Redis 持久化:RDB 和 AOF 的 “爱恨情仇”
  • 多源最短路(Floyd算法
  • 【数据结构——图(例图篇)】
  • 安卓俄罗斯方块,经典拖动双模式体验
  • 21th cpp think
  • 收集飞花令碎片——C语言关键字typedef
  • Python/JS/Go/Java同步学习(第十二篇)四语言“字符串填充编号“对照表: 财务“小南“纸式填充术加凭证编号崩溃(附源码/截图/参数表/避坑指南)
  • 工具变量-5G试点城市DID数据(2014-2025年
  • 金融数学专业需要学哪些数学和编程内容?
  • 【算法】【链表】148.排序链表--通俗讲解
  • Linux 内核镜像与启动组件全解析:从 vmlinux 到 extlinux.conf
  • HIS架构智能化升级编程路径:从底层原理到临床实践的深度解析(上)
  • leetcode-加油站
  • Coze源码分析-资源库-创建知识库-前端源码-总结
  • 【PHP7内核剖析】-1.2 执行流程
  • Java 多线程进阶(四)-- 锁策略,CAS,synchronized的原理,JUC当中常见的类
  • 从ENIAC到Linux:计算机技术与商业模式的协同演进
  • UE5版本Windows构建pc平台报错googletest的问题记录
  • 【LeetCode】杨辉三角,轮转数组,洗牌算法
  • 5.Three.js 学习(基础+实践)
  • 在 React 中如何使用 useMemo 和 useCallback 优化性能?
  • C++20多线程新特性:更安全高效的并发编程
  • 结构光三维重建原理详解(1)
  • window显示驱动开发—视频呈现网络简介
  • Vision Transformer (ViT) :Transformer在computer vision领域的应用(二)
  • 计算机网络的基本概念-2