当前位置:首页 >娱乐 >【数据结构之线索二叉树】线索二叉树的原理及创建 总共会有 n+1 个空指针域

【数据结构之线索二叉树】线索二叉树的原理及创建 总共会有 n+1 个空指针域

2024-06-30 22:05:07 [百科] 来源:避面尹邢网

【数据结构之线索二叉树】线索二叉树的数据索叉树线索叉树原理及创建

作者:行小观 大数据 线索二叉树充分利用了二叉树中的空指针域,给予二叉树一个新特性——通过一次遍历进行线索化后,结构及创建二叉树中就能保存其结点之间的原理前驱和后继关系。

[[396654]]

本文转载自微信公众号「二十二画程序员」,数据索叉树线索叉树作者行小观。结构及创建转载本文请联系二十二画程序员公众号。原理  

【数据结构之线索二叉树】线索二叉树的原理及创建 总共会有  n+1 个空指针域

 1. 为什么要用到线索二叉树?数据索叉树线索叉树

我们先来看看普通的二叉树有什么缺点。下面是结构及创建一个普通二叉树(链式存储方式):

【数据结构之线索二叉树】线索二叉树的原理及创建 总共会有  n+1 个空指针域

【数据结构之线索二叉树】线索二叉树的原理及创建 总共会有  n+1 个空指针域

一颗普通二叉树

乍一看,会不会有一种违和感?原理整个结构一共有 7 个结点,总共 14 个指针域,数据索叉树线索叉树其中却有 8 个指针域都是结构及创建空的。对于一颗有 n 个结点的原理二叉树而言,总共会有 n+1 个空指针域,数据索叉树线索叉树这个规律使用所有的结构及创建二叉树。

这么多的原理空指针域是不是显得很浪费?我们学习数据结构和算法的重点就是在想法设法地提高时间效率和空间利用率。这么多的指针域就这么白白浪费了,太败家了!

所以我们要想法子好好利用它们,利用它们来帮助我们更好地使用二叉树这个数据结构。

那么如何利用呢?

前面已经强调过很多次了,遍历二叉树的实质是将二叉树中非线性结构的结点转化为线性的序列,然后才能方便我们遍历。

比如上图的中序遍历序列为:DBGEACF。

对于一个线性序列(线性表)来说,它有直接前驱和直接后继的概念(在【什么是线性表?】中介绍过)。比如在中序遍历序列中,B 的直接前驱为 D,直接后继为 G。

我们之所以能知道 B 的直接前驱和直接后继,是因为我们按照中序遍历的算法,把二叉树的中序遍历序列写出来了,然后根据这个顺序序列说谁的前驱是谁、后继是谁。

直接前驱和直接后继是不能完全直接通过二叉树得到的,因为二叉树中只有双亲和孩子结点之间的直接关系,即二叉树的结点指针域中只存储了其孩子结点的地址。

现在的需求是,我想能直接从二叉树上得到某结点在中序遍历方式下的直接前驱和直接后继。

这时候就需要用到线索二叉树了。

2. 什么是线索二叉树?

当然,我们肯定需要借助结点的指针域来保存直接前驱和直接后继的地址。

其实,在上图的普通二叉树中(以中序遍历得到的序列),部分结点(指针域不为空的结点)是可以找到其直接前驱或后继的,比如结点 E 的左孩子 G 就是结点 E 的直接前驱;结点 A 的右孩子 C 就是结点 A 的直接后继。

但部分结点(指针域为空)是行不通的,比如结点 G 的直接后继是 E,直接前驱是 B,但在二叉树中却不能得出这样的结论。怎么办呢?我们注意到,结点 G 的两个指针域都为 NULL,并未被利用,那么我们使用这两个指针,分别指向其前驱和后继不就好了吗?

中序遍历下结点G的指向情况

实在是两全其美,天作之合!但是问题并没有解决!

因为我们是利用空指针域来指向前驱或后继的,对于那些指针域不为空的结点,这样是矛盾的,比如结点 E 和结点 B。

既然有矛盾,那么我们就发现产生矛盾的根源,解决矛盾。

产生矛盾的根源是:结点的指针域为空和不为空时,指针的指向矛盾。即,指针不为空时指向孩子和指针为空时指向前驱或后继之间的矛盾。

那么我们对症下药,把指针域为空和不为空给区分出来,清晰地告诉指针:不为空时指向孩子,为空时指向前驱或后继。这就需要我们给两个指针各添加一个标志位。

线索二叉树的结点

并约定以下规则:

  • left_flag == 0 时,指针 left_child 指向左孩子
  • left_flag == 1 时,指针 left_child 指向直接前驱
  • right_flag == 0 时,指针 right_child 指向右孩子
  • right_flag == 1 时,指针 right_child 指向直接前驱

二叉树的结点要有所变化:

  1. /*线索二叉树的结点的结构体*/ 
  2. typedef struct Node {  
  3.     char data; //数据域 
  4.     struct Node *left_child; //左指针域 
  5.     int left_flag; //左指针标志位 
  6.     struct Node *right_child; //右指针域 
  7.     int right_flag; //右指针标志位 
  8. } TTreeNode; 

有了标志位,一切就能理清了。我们称指向直接前驱和后继的指针为线索。标志位为 0 的指针是指向孩子的指针,标志位为 1 的指针是线索。

一个二叉链表树,结点结构如上,我们将所有空指针都变为线索,这样的二叉树就是二叉线索树。

3. 如何创造线索二叉树?

在普通二叉树中,我们想要获取某个结点在某种遍历次序下的直接前驱或后继,每次都需要遍历获取到遍历次序之后才能知道。而在线索二叉树中,我们只需要遍历一次(创造线索二叉树时的遍历),之后,线索二叉树就能“记住”每个结点的直接前驱和后继了,以后都不需要再通过遍历次序获取前驱或后继了。

我们按照某种遍历方式,把普通二叉树变为线索二叉树的过程被称为二叉树的线索化。

接下来,我们用中序遍历的方式,将下面的二叉树线索化为线索二叉树

将标志位为 1 的指针,按照中序遍历序列,使其指向前驱或后继:

其中,结点 D 没有直接前驱,结点 F 没有直接后继,故指针为 NULL。

到此,我们算是解决了拥有 n 个结点的二叉树存在 n+1 个空指针域所造成的浪费,解决方式是给每个结点的指针增加一个标志位,以此来利用空指针域。标志位中存储的是 0 或 1 的布尔值,与浪费的空指针域相比,是相对比较划算的。而且使二叉树具有了一种新特性——二叉树中能保存在某种遍历次序下的结点之间的前驱和后继关系。

4. 线索化的实现

请注意一点,线索二叉树是由普通二叉树得来的,而且是按某种遍历顺序得来的。因为线索是在知道某个结点的前驱和后继的情况下才能设置,而前驱和后继关系不能通过二叉树直接体现,只能通过遍历二叉树得到的线性序列得出关系。所以要通过某种遍历方式得到具有前驱和后继关系的序列后,才能修改结点的空指针,进而设置线索。

即:线索化的实质是在按照某种遍历次序进行遍历二叉树的过程中修改结点的空指针,使其指向其在该遍历次序下的直接前驱或直接后继的过程。

我们在【二叉树的遍历原理】和【二叉树的遍历实现】分别介绍了二叉树四种遍历方式的原理及代码实现。当时我们是以打印为例来介绍遍历的。但遍历不止做打印的事,还可以做线索化的事。

所以,代码的大体结构还是一样的,我们只需把遍历代码中的打印代码换成线索化的代码,并作出一些其他改变即可。

下面以下图为例,分别介绍三种线索化:

一颗未线索化的二叉树,其所有标志位均默认为 0.

示例

4.1. 中序线索化

按照中序遍历次序线索化后,可得下图:

我们先再次明确以下内容:

  • 我们是在遍历二叉树的过程中进行线索化的。
  • 中序遍历的顺序为:左子树 >> 根 >> 右子树。
  • 线索化修改两个东西:空指针域和其对应的标志位。
  • 如何修改?将空指针域置为直接前驱或后继。

所以我们的问题变成了:

  1. 找到所有空指针域。
  2. 找到空指针域所属结点,在先序次序下的直接前驱和直接后继。
  3. 修改空指针域的内容,及其标志位,使该指针称为线索。

说明:我们在遍历二叉树时,使用到了递归,所以在进行线索化的时候,也会使用它。

具体代码如下:

  1. //全局变量 prev 指针,指向刚访问过的结点 
  2. TTreeNode *prev = NULL; 
  3.  
  4. /** 
  5.  * 中序线索化 
  6.  */ 
  7. void inorder_threading(TTreeNode *root) 
  8. {  
  9.     if (root == NULL) {  //若二叉树为空,做空操作 
  10.         return; 
  11.     } 
  12.     inorder_threading(root->left_child); 
  13.     if (root->left_child == NULL) {  
  14.         root->left_flag = 1; 
  15.         root->left_child = prev; 
  16.     } 
  17.     if (prev != NULL && prev->right_child == NULL) {  
  18.         prev->right_flag = 1; 
  19.         prev->right_child = root; 
  20.     } 
  21.     prev = root; 
  22.     inorder_threading(root->right_child); 

4.2. 先序线索化

按照先序顺序线索化后,可得下图:

具体代码如下:

  1. // 全局变量 prev 指针,指向刚访问过的结点 
  2. TTreeNode *prev = NULL; 
  3.  
  4. /** 
  5.  * 先序线索化 
  6.  */ 
  7. void preorder_threading(TTreeNode *root) 
  8. {  
  9.     if (root == NULL) {  
  10.         return; 
  11.     } 
  12.     if (root->left_child == NULL) {  
  13.         root->left_flag = 1; 
  14.         root->left_child = prev; 
  15.     } 
  16.     if (prev != NULL && prev->right_child == NULL) {  
  17.         prev->right_flag = 1; 
  18.         prev->right_child = root; 
  19.     } 
  20.     prev = root; 
  21.     if (root->left_flag == 0) {  
  22.         preorder_threading(root->left_child); 
  23.     } 
  24.     if (root->right_flag == 0) {  
  25.         preorder_threading(root->right_child); 
  26.     } 

4.3. 后序线索化

按照后序遍历次序线索化后,可得下图:

具体代码如下:

  1. //全局变量 prev 指针,指向刚访问过的结点 
  2. TTreeNode *prev = NULL; 
  3.  
  4. /** 
  5.  * 后序线索化 
  6.  */ 
  7. void postorder_threading(TTreeNode *root) 
  8. {  
  9.     if (root == NULL) {  
  10.         return; 
  11.     } 
  12.     postorder_threading(root->left_child); 
  13.     postorder_threading(root->right_child); 
  14.     if (root->left_child == NULL) {  
  15.         root->left_flag = 1; 
  16.         root->left_child = prev; 
  17.     } 
  18.     if (prev != NULL && prev->right_child == NULL) {  
  19.         prev->right_flag = 1; 
  20.         prev->right_child = root; 
  21.     } 
  22.     prev = root; 

5. 总结

线索二叉树充分利用了二叉树中的空指针域,给予二叉树一个新特性——通过一次遍历进行线索化后,二叉树中就能保存其结点之间的前驱和后继关系。

所以,如果我们需要频繁遍历二叉树,查找某个结点的直接前驱或后继结点,使用线索二叉树是非常合适的。

此外,由于代码涉及到递归,初次接触可能不好理解,我们可以借助断点进行调试,细致观察线索化的整个过程来帮助理解。

 

责任编辑:武晓燕 来源: 二十二画程序员 数据结构创建

(责任编辑:娱乐)

    推荐文章
    • 网商贷额度突然降低了怎么办 提高店铺评分还能提高额度吗?

      网商贷额度突然降低了怎么办 提高店铺评分还能提高额度吗?在发展事业的过程中,对于一些家底不是很雄厚的个体户来说,想要扩大经营规模,免不了要去申请贷款。支付宝旗下的网商贷,就是一种经营性贷款。网商贷额度突然降低了怎么办?这样做或许能快速恢复额度!网商贷额度突 ...[详细]
    • 如何在用户离开页面时可靠地发送 HTTP 请求

      如何在用户离开页面时可靠地发送 HTTP 请求如何在用户离开页面时可靠地发送 HTTP 请求作者:张张 2022-07-03 17:55:53网络 通信技术 您可能希望该请求的分派是同步的,之后我们将继续导航离开页面,而其他服务器成功地处理该请求 ...[详细]
    • 十大数据存储问题及解决方法

      十大数据存储问题及解决方法十大数据存储问题及解决方法作者:Paul Kirvan 2021-08-13 14:29:52存储 企业不要忽视一些主要的数据存储问题,其中包括与人员、安全和成本相关的问题,而如果没有适当的规划,它 ...[详细]
    • 戴尔推出智能数据管理解决方案应对虚拟时代

      戴尔推出智能数据管理解决方案应对虚拟时代戴尔推出智能数据管理解决方案应对虚拟时代作者:NEO 2010-04-08 18:07:48服务器 数据管理 戴尔今天宣布了其智能数据管理解决方案,以帮助客户从容应对数据存储和管理领域前所未有的挑战, ...[详细]
    • 中国石化一季度全力优化生产经营 业绩取得高质量开门红

      中国石化一季度全力优化生产经营 业绩取得高质量开门红4月27日,中国石化发布2022年一季度业绩报告。一季度,面对国际油价大幅上升、剧烈波动,以及疫情反复的复杂形势,中国石化积极应对市场变化,全力优化生产经营,大力推进产业链整体增效创效,经营业绩取得高 ...[详细]
    • 生产环境内存溢出了!!!你会解决吗?

      生产环境内存溢出了!!!你会解决吗?生产环境内存溢出了!!!你会解决吗?作者:冰河 2021-08-26 05:00:44存储 存储软件 既然JDK1.6中的String类存在如此巨大的坑,那最直接有效的方式就是升级JDK。于是,我便跟 ...[详细]
    • 易瞳科技获数千万Pre

      易瞳科技获数千万Pre5月12日消息,专注介导现实(MR)智能眼镜研发的易瞳科技获得由中科乐创领投、艾瑞资本跟投的数千万人民币Pre-A轮融资。据创投时报了解,易瞳科技曾于2015年5月年获得Pre-Angel和乐博资本的 ...[详细]
    • kubernetes生产环境优秀实践

      kubernetes生产环境优秀实践kubernetes生产环境优秀实践作者:ikubernetes 2022-09-01 08:50:22云计算 云原生 本文仅提供在kubernetes上部署安全、可扩展和弹性服务的可行性最佳实践。仅 ...[详细]
    • 安逸花还清后还收费吗 取消方法是怎样的?

      安逸花还清后还收费吗 取消方法是怎样的?大家应该都知道,贷款都是要成本的,贷款平台会在借款成功后收取一定的费用,在安逸花借钱也一样。有不少人在安逸花上借的钱还清了想知道还会不会再收费,那么安逸花还清后还收费吗?这个要看具体是什么费用了,一起 ...[详细]
    • 在Kubernetes集群中部署MetalLB实现负载均衡

      在Kubernetes集群中部署MetalLB实现负载均衡在Kubernetes集群中部署MetalLB实现负载均衡作者:ikubernetes 2022-08-31 08:30:32云计算 云原生 我们通过helm去部署一些服务时,尝尝会依赖于LoadBa ...[详细]
    热点阅读