行业动态 【数据组织之线索二叉树】线索二叉树的原理及创建

本文转载自微信公多号「二十二画程序员」,作者走幼不都雅。转载本文请有关二十二画程序员公多号。  

日前,《黑神话:悟空》战斗策划离职引起了不小的风波,引得游戏科学的联合创始人杨奇在微博上进行了一次回复。不过由于最近部分媒体和自媒体对该事件的过度报道,为此杨奇先生又专门发了一条微博来回应此事件。

《奇异人生》系列的新作《奇异人生:本色》发布了最新宣传片,介绍了游戏主角所在的Haven Springs小镇。玩家可以在镇子里进行游览、游玩小游戏等活动,也可以与NPC交流并逐步发现隐藏的剧情。一起来看看吧!

开发商Heart Machine的冒险游戏《太阳灰国》放出了新的发售日期预告,游戏将于2021年10月26日登陆PS4/PS5/EPIC商城。

拳头公司曾在2018年就因性骚扰和性别歧视遭到起诉,最后支付了1000万美元的赔偿费用进行了和解。后来拳头又试图将案件转移至私人仲裁听证会,最后因员工以罢工相要挟才放弃强制仲裁。

今日,万代南梦宫在官方油管频道上发布了《破晓传说》中角色奇莎兰的个人预告,简单展示了一小部分游戏过场与角色实机游玩。下面为大家带来预告视频。

 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 指向直接前驱

二叉树的结点要有所转折:

/*线索二叉树的结点的组织体*/ typedef struct Node {     char data; //数据域     struct Node *left_child; //左指针域     int left_flag; //左指针标志位     struct Node *right_child; //右指针域     int right_flag; //右指针标志位 } TTreeNode; 

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

一个二叉链外树,结点组织如上,吾们将所有空指针都变为线索,云云的二叉树就是二叉线索树。

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

在清淡二叉树中,吾们想要获取某个结点在某栽遍历顺序下的直接前驱或后继,每次都必要遍历获取到遍历顺序之后才能清新。而在线索二叉树中,吾们只必要遍历一次(创造线索二叉树时的遍历),之后,线索二叉树就能“记住”每个结点的直接前驱和后继了,以后都不必要再始末遍历顺序获取前驱或后继了。

吾们遵命某栽遍历手段,把清淡二叉树变为线索二叉树的过程被称为二叉树的线索化。

接下来,吾们用中序遍历的手段,将下面的二叉树线索化为线索二叉树

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

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

到此,吾们算是解决了拥有 n 个结点的二叉树存在 n+1 个空指针域所造成的铺张,解决手段是给每个结点的指针增补一个标志位,以此来行使空指针域。标志位中存储的是 0 或 1 的布尔值,与铺张的空指针域相比,是相对比较划算的。而且使二叉树具有了一栽新特性——二叉树中能保存在某栽遍历顺序下的结点之间的前驱和后继有关。

4. 线索化的实现

请仔细一点,线索二叉树是由清淡二叉树得来的,而且是按某栽遍历挨次得来的。由于线索是在清新某个结点的前驱和后继的情况下才能竖立,而前驱和后继有关不及始末二叉树直接表现,只能始末遍历二叉树得到的线性序列得出有关。于是要始末某栽遍历手段得到具有前驱和后继有关的序列后,才能修改结点的空指针,进而竖立线索。

即:线索化的内心是在遵命某栽遍历顺序进走遍历二叉树的过程中修改结点的空指针,使其指向其在该遍历顺序下的直接前驱或直接后继的过程。

吾们在【二叉树的遍历原理】和【二叉树的遍历实现】别离介绍了二叉树四栽遍历手段的原理及代码实现。那时吾们是以打印为例来介绍遍历的。但遍历不止做打印的事,还能够做线索化的事。

于是,代码的大体组织照样相通的,吾们只需把遍历代码中的打印代码换成线索化的代码,并作出一些其他转折即可。

下面以下图为例,别离介绍三栽线索化:

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

示例

4.1. 中序线索化

遵命中序遍历顺序线索化后,可得下图:

吾们先再次清晰以下内容:

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

于是吾们的题目变成了:

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

表明:吾们在遍历二叉树时,操纵到了递归,于是在进走线索化的时候,也会操纵它。

详细代码如下:

//全局变量 prev 指针,指向刚访问过的结点 TTreeNode *prev = NULL;  /**  * 中序线索化  */ void inorder_threading(TTreeNode *root) {     if (root == NULL) { //若二叉树为空,做空操作         return;     }     inorder_threading(root->left_child);     if (root->left_child == NULL) {         root->left_flag = 1;         root->left_child = prev;     }     if (prev != NULL && prev->right_child == NULL) {         prev->right_flag = 1;         prev->right_child = root;     }     prev = root;     inorder_threading(root->right_child); } 

4.2. 先序线索化

遵命先序挨次线索化后,可得下图:

详细代码如下:

// 全局变量 prev 指针,指向刚访问过的结点 TTreeNode *prev = NULL;  /**  * 先序线索化  */ void preorder_threading(TTreeNode *root) {     if (root == NULL) {         return;     }     if (root->left_child == NULL) {         root->left_flag = 1;         root->left_child = prev;     }     if (prev != NULL && prev->right_child == NULL) {         prev->right_flag = 1;         prev->right_child = root;     }     prev = root;     if (root->left_flag == 0) {         preorder_threading(root->left_child);     }     if (root->right_flag == 0) {         preorder_threading(root->right_child);     } } 

4.3. 后序线索化

遵命后序遍历顺序线索化后,可得下图:

详细代码如下:

//全局变量 prev 指针,指向刚访问过的结点 TTreeNode *prev = NULL;  /**  * 后序线索化  */ void postorder_threading(TTreeNode *root) {     if (root == NULL) {         return;     }     postorder_threading(root->left_child);     postorder_threading(root->right_child);     if (root->left_child == NULL) {         root->left_flag = 1;         root->left_child = prev;     }     if (prev != NULL && prev->right_child == NULL) {         prev->right_flag = 1;         prev->right_child = root;     }     prev = root; } 
5. 总结

线索二叉树足够行使了二叉树中的空指针域,给予二叉树一个新特性——始末一次遍历进走线索化后,二叉树中就能保存其结点之间的前驱和后继有关。

于是,倘若吾们必要屡次遍历二叉树,查找某个结点的直接前驱或后继结点,操纵线索二叉树是专门正当的。

此外,由于代码涉及到递归,初次接触能够不益理解,吾们能够借助断点进走调试,详细不都雅察线索化的整个过程来协助理解。

【编辑选举】行业动态

Google发布语义分割新数据集!顺带开发个模型屠榜,已被CVPR2021授与 打破 SOC 的壁垒,升迁数据坦然性 Passwordstate暗号管理器遭到侵犯将影响2.9万多家企业的数据坦然 追求性数据分析:决定人造智能与机器学习凶果的第一步 打破 SOC 的壁垒,升迁数据坦然性 - 网络·坦然技术周刊第485期