数据结构:线索二叉树

线索二叉树

通过前面对二叉树的学习,了解到二叉树本身是一种非线性结构,采用任何一种遍历二叉树的方法,都可以得到树中所有结点的一个线性序列。在这个序列中,除第一个结点外,每个结点都有自己的直接前趋;除最后一个结点外,每个结点都有一个直接后继。

在这里插入图片描述

例如,上图采用先序遍历的方法得到的结点序列为:1 2 4 5 3 6 7,在这个序列中,结点 2 的直接前趋结点为 1,直接后继结点为 4

什么是线索二叉树

如果算法中多次涉及到对二叉树的遍历,普通的二叉树就需要使用栈结构做重复性的操作。

线索二叉树不需要如此,在遍历的同时,使用二叉树中空闲的内存空间记录某些结点的前趋和后继元素的位置(不是全部)。这样在算法后期需要遍历二叉树时,就可以利用保存的结点信息,提高了遍历的效率。使用这种方法构建的二叉树,即为“线索二叉树”。

线索二叉树的结点结构

如果在二叉树中想保存每个结点前趋和后继所在的位置信息,最直接的想法就是改变结点的结构,即添加两个指针域,分别指向该结点的前趋和后继。

但是这种方式会降低树存储结构的存储密度。而对于二叉树来讲,其本身还有很多未利用的空间。

存储密度指的是数据本身所占的存储空间和整个结点结构所占的存储量之比。

每一棵二叉树上,很多结点都含有未使用的指向NULL的指针域。除了度为2的结点,度为 1 的结点,有一个空的指针域;叶子结点两个指针域都为NULL

规律:在有 n 个结点的二叉链表中必定存在 n+1 个空指针域。

线索二叉树实际上就是使用这些空指针域来存储结点之间前趋和后继关系的一种特殊的二叉树。

线索二叉树中,如果结点有左子树,则 lchild 指针域指向左孩子,否则 lchild 指针域指向该结点的直接前趋;同样,如果结点有右子树,则 rchild 指针域指向右孩子,否则 rchild 指针域指向该结点的直接后继。

为了避免指针域指向的结点的意义混淆,需要改变结点本身的结构,增加两个标志域,如下图2所示。线索二叉树中的结点结构

图2

在这里插入图片描述

LTagRTag 为标志域。实际上就是两个布尔类型的变量:

  • LTag 值为 0 时,表示 lchild 指针域指向的是该结点的左孩子;为 1 时,表示指向的是该结点的直接前趋结点;
  • RTag 值为 0 时,表示 rchild 指针域指向的是该结点的右孩子;为 1 时,表示指向的是该结点的直接后继结点。

结点结构代码实现:

#define TElemType int//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum PointerTag{Link,Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode{TElemType data;//数据域struct BiThrNode* lchild,*rchild;//左孩子,右孩子指针域PointerTag Ltag,Rtag;//标志域,枚举类型
}BiThrNode,*BiThrTree;

表示二叉树时,使用上图所示的结点结构构成的二叉链表,被称为线索链表;构建的二叉树称为线索二叉树

线索链表中的“线索”,指的是链表中指向结点前趋和后继的指针。二叉树经过某种遍历方法转化为线索二叉树的过程称为线索化。

对二叉树进行线索化

将二叉树转化为线索二叉树,实质上是在遍历二叉树的过程中,将二叉链表中的空指针改为指向直接前趋或者直接后继的线索。

线索化的过程即为在遍历的过程中修改空指针的过程。

在遍历过程中,如果当前结点没有左孩子,需要将该结点的 lchild 指针指向遍历过程中的前一个结点,所以在遍历过程中,设置一个指针(名为 pre ),时刻指向当前访问结点的前一个结点。代码实现(拿中序遍历为例):

//中序对二叉树进行线索化
void InThreading(BiThrTree p){
//如果当前结点存在
if (p) {InThreading(p->lchild);//递归当前结点的左子树,进行线索化//如果当前结点没有左孩子,左标志位设为1,左指针域指向上一结点 pre
if (!p->lchild) {p->Ltag=Thread;p->lchild=pre;}
//如果 pre 没有右孩子,右标志位设为 1,右指针域指向当前结点。
if (!pre->rchild) {pre->Rtag=Thread;pre->rchild=p;}pre=p;//线索化完左子树后,让pre指针指向当前结点InThreading(p->rchild);//递归右子树进行线索化}
}

注意:中序对二叉树进行线索化的过程中,在两个递归函数中间的运行程序,和之前介绍的中序遍历二叉树的输出函数的作用是相同的。

将中间函数移动到两个递归函数之前,就变成了前序对二叉树进行线索化的过程;后序线索化同样如此。

线索二叉树遍历

3 中是一个按照中序遍历建立的线索二叉树。其中,实线表示指针,指向的是左孩子或者右孩子。虚线表示线索,指向的是该结点的直接前趋或者直接后继。

在这里插入图片描述

图 3 线索二叉树

使用线索二叉树时,会经常遇到一个问题,如图 3 中,结点 b b b的直接后继直接通过指针域获得,为结点 ∗ * ;而由于结点 ∗ * 的度为2 ,无法利用指针域指向后继结点,整个链表断掉了。当在遍历过程,遇到这种问题是解决的办法就是:寻找先序、中序、后序遍历的规律,找到下一个结点。

在先序遍历过程中,如果结点因为有右孩子导致无法找到其后继结点,如果结点有左孩子,则后继结点是其左孩子;否则,就一定是右孩子。拿图 3 举例,结点 + 的后继结点是其左孩子结点 a ,如果结点 a 不存在的话,就是结点 *

在中序遍历过程中,结点的后继是遍历其右子树时访问的第一个结点,也就是右子树中位于最左下的结点。例如图 3 中结点 * ,后继结点为结点 c ,是其右子树中位于最左边的结点。反之,结点的前趋是左子树最后访问的那个结点。

后序遍历中找后继结点需要分为 3 种情况:

  1. 如果该结点是二叉树的根,后继结点为空;
  2. 如果该结点是父结点的右孩子(或者是左孩子,但是父结点没有右孩子),后继结点是父结点;
  3. 如果该结点是父结点的左孩子,且父结点有右子树,后继结点为父结点的右子树在后序遍历列出的第一个结点。

使用后序遍历建立的线索二叉树,在真正使用过程中遇到链表的断点时,需要访问父结点,所以在初步建立二叉树时,宜采用三叉链表做存储结构。

遍历线索二叉树非递归代码实现:

//中序遍历线索二叉树
void InOrderThraverse_Thr(BiThrTree p)
{while(p){
//一直找左孩子,最后一个为中序序列中排第一的while(p->Ltag == Link){p = p->lchild;}cout << p->data;//操作结点数据//当结点右标志位为1时,直接找到其后继结点while(p->Rtag == Thread && p->rchild !=NULL){p = p->rchild;cout <<  p->data;}
//否则,按照中序遍历的规律,找其右子树中最左下的结点,也就是继续循环遍历p = p->rchild;}
}

整节完整代码

#include "iostream"
using namespace std;
#define TElemType char//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum {Link,Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode {TElemType data;//数据域struct BiThrNode* lchild, *rchild;//左孩子,右孩子指针域PointerTag Ltag, Rtag;//标志域,枚举类型
}BiThrNode, *BiThrTree;BiThrTree pre = NULL;//采用前序初始化二叉树
//中序和后序只需改变赋值语句的位置即可
void CreateTree(BiThrTree * tree) {char data;cin >> data;if (data != '#') {if (!((*tree) = (BiThrNode*)malloc(sizeof(BiThrNode)))) {cout << "申请结点空间失败";return;}else {(*tree)->data = data;//采用前序遍历方式初始化二叉树(*tree)->Ltag = Link;(*tree)->Rtag = Link;CreateTree(&((*tree)->lchild));//初始化左子树CreateTree(&((*tree)->rchild));//初始化右子树}}else {*tree = NULL;}
}
//中序对二叉树进行线索化
void InThreading(BiThrTree p) {//如果当前结点存在if (p) {InThreading(p->lchild);//递归当前结点的左子树,进行线索化//如果当前结点没有左孩子,左标志位设为1,左指针域指向上一结点 preif (!p->lchild) {p->Ltag = Thread;p->lchild = pre;}//如果 pre 没有右孩子,右标志位设为 1,右指针域指向当前结点。if (pre && !pre->rchild) {pre->Rtag = Thread;pre->rchild = p;}pre = p;//pre指向当前结点InThreading(p->rchild);//递归右子树进行线索化}
}
//中序遍历线索二叉树
void InOrderThraverse_Thr(BiThrTree p)
{while (p){//一直找左孩子,最后一个为中序序列中排第一的while (p->Ltag == Link) {p = p->lchild;}cout << p->data;//当结点右标志位为1时,直接找到其后继结点while (p->Rtag == Thread && p->rchild != NULL){p = p->rchild;cout << p->data;}//否则,按照中序遍历的规律,找其右子树中最左下的结点,也就是继续循环遍历p = p->rchild;}
}int main() {BiThrTree t;cout << "输入前序二叉树:" << '\n';CreateTree(&t);InThreading(t);cout << "输出中序序列:" << '\n';InOrderThraverse_Thr(t);return 0;
}

运行结果

输入前序二叉树:
124###35##6##
输出中序序列:
4 2 1 5 3 6

通过前一节对线索二叉树的学习,其中,在遍历使用中序序列创建的线索二叉树时,对于其中的每个结点,即使没有线索的帮助下,也可以通过中序遍历的规律找到直接前趋和直接后继结点的位置。

也就是说,建立的线索二叉链表可以从两个方向对结点进行中序遍历。通过前一节的学习,线索二叉链表可以从第一个结点往后逐个遍历。但是起初由于没有记录中序序列中最后一个结点的位置,所以不能实现从最后一个结点往前逐个遍历。

双向线索链表的作用就是可以让线索二叉树从两个方向实现遍历。

双向线索二叉树的实现过程

在线索二叉树的基础上,额外添加一个结点。此结点的作用类似于链表中的头指针,数据域不起作用,只利用两个指针域(由于都是指针,标志域都为 0 )。

左指针域指向二叉树的树根,确保可以正方向对二叉树进行遍历;同时,右指针指向线索二叉树形成的线性序列中的最后一个结点。

这样,二叉树中的线索链表就变成了双向线索链表,既可以从第一个结点通过不断地找后继结点进行遍历,也可以从最后一个结点通过不断找前趋结点进行遍历。

在这里插入图片描述

图4 双向线索二叉链表

代码实现:

//建立双向线索链表
void InOrderThread_Head(BiThrTree *h, BiThrTree t)
{
//初始化头结点(*h) = (BiThrTree)malloc(sizeof(BiThrNode));if((*h) == NULL){cout << "申请内存失败";return ;}(*h)->rchild = *h;(*h)->Rtag = Link;
//如果树本身是空树
if(!t){(*h)->lchild = *h;(*h)->Ltag = Link;}else{pre = *h;//pre指向头结点(*h)->lchild = t;//头结点左孩子设为树根结点(*h)->Ltag = Link;InThreading(t);//线索化二叉树,pre结点作为全局变量,线索化结束后,pre结点指向中序序列中最后一个结点pre->rchild = *h;pre->Rtag = Thread;(*h)->rchild = pre;}
}

双向线索二叉树的遍历

双向线索二叉树遍历时,如果正向遍历,就从树的根结点开始。整个遍历过程结束的标志是:当从头结点出发,遍历回头结点时,表示遍历结束。

//中序正向遍历双向线索二叉树
void InOrderThraverse_Thr(BiThrTree h)
{BiThrTree p;p = h->lchild;//p指向根结点
while(p != h){while(p->Ltag == Link)//当ltag = 0时循环到中序序列的第一个结点{p = p->lchild;}cout << p->data;//显示结点数据,可以更改为其他对结点的操作
while(p->Rtag == Thread && p->rchild != h){p = p->rchild;cout << p->data;}p = p->rchild;//p进入其右子树}
}

逆向遍历线索二叉树的过程即从头结点的右指针指向的结点出发,逐个寻找直接前趋结点,结束标志同正向遍历一样:

//中序逆方向遍历线索二叉树
void InOrderThraverse_Thr(BiThrTree h){BiThrTree p;p=h->rchild;while (p!=h) {while (p->Rtag==Link) {p=p->rchild;}cout << p->data;
//如果lchild为线索,直接使用,输出
while (p->Ltag==Thread && p->lchild !=h) {p=p->lchild;cout << p->data;}p=p->lchild;}
}

完整代码实现

#include "iostream"
using namespace std;
#define TElemType char//宏定义,结点中数据域的类型
//枚举,Link为0,Thread为1
typedef enum {Link,Thread
}PointerTag;
//结点结构构造
typedef struct BiThrNode {TElemType data;//数据域struct BiThrNode* lchild, *rchild;//左孩子,右孩子指针域PointerTag Ltag, Rtag;//标志域,枚举类型
}BiThrNode, *BiThrTree;BiThrTree pre = NULL;//采用前序初始化二叉树
//中序和后序只需改变赋值语句的位置即可
void CreateTree(BiThrTree * tree) {char data;cin >> data;if (data != '#') {if (!((*tree) = (BiThrNode*)malloc(sizeof(BiThrNode)))) {cout << "申请结点空间失败";return;}else {(*tree)->data = data;//采用前序遍历方式初始化二叉树(*tree)->Ltag = Link;(*tree)->Rtag = Link;CreateTree(&((*tree)->lchild));//初始化左子树CreateTree(&((*tree)->rchild));//初始化右子树}}else {*tree = NULL;}
}
//中序对二叉树进行线索化
void InThreading(BiThrTree p) {//如果当前结点存在if (p) {InThreading(p->lchild);//递归当前结点的左子树,进行线索化//如果当前结点没有左孩子,左标志位设为1,左指针域指向上一结点 preif (!p->lchild) {p->Ltag = Thread;p->lchild = pre;}//如果 pre 没有右孩子,右标志位设为 1,右指针域指向当前结点。if (pre && !pre->rchild) {pre->Rtag = Thread;pre->rchild = p;}pre = p;//pre指向当前结点InThreading(p->rchild);//递归右子树进行线索化}
}
//建立双向线索链表
void InOrderThread_Head(BiThrTree *h, BiThrTree t)
{//初始化头结点(*h) = (BiThrTree)malloc(sizeof(BiThrNode));if ((*h) == NULL) {cout << "申请内存失败";return;}(*h)->rchild = *h;(*h)->Rtag = Link;//如果树本身是空树if (!t) {(*h)->lchild = *h;(*h)->Ltag = Link;}else {pre = *h;//pre指向头结点(*h)->lchild = t;//头结点左孩子设为树根结点(*h)->Ltag = Link;InThreading(t);//线索化二叉树,pre结点作为全局变量,线索化结束后,pre结点指向中序序列中最后一个结点pre->rchild = *h;pre->Rtag = Thread;(*h)->rchild = pre;}
}
//中序正向遍历双向线索二叉树
void InOrderThraverse_Thr(BiThrTree h)
{BiThrTree p;p = h->lchild;           //p指向根结点while (p != h){while (p->Ltag == Link)   //当ltag = 0时循环到中序序列的第一个结点{p = p->lchild;}cout << p->data;while (p->Rtag == Thread && p->rchild != h){p = p->rchild;cout << p->data;}p = p->rchild;           //p进入其右子树}
}
int main() {BiThrTree t;BiThrTree h;cout << "输入前序二叉树:" << '\n';CreateTree(&t);InOrderThread_Head(&h, t);cout << "输出中序二叉树:" << '\n';InOrderThraverse_Thr(h);return 0;
}

运行结果:

输入前序二叉树:
124###35##6##
输出中序序列:
4 2 1 5 3 6

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

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

相关文章

记录Selenium自动化测试过程中接口的调用信息

上一篇博客&#xff0c;我写了python自动化框架的一些知识和粗浅的看法&#xff0c;在上一篇中我也给自己提出一个需求&#xff1a;如果记录在测试过程中接口的调用情况&#xff1f;提出这个需求&#xff0c;我觉得是有意义的。你在测试过程中肯定会遇到一些莫名其妙的问题&…

【JAVA】 String 类简述笔记

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️初识JAVA】 文章目录 前言String类创建一个String类 常用方法字符串长度 length() 方法连接字符串 concat() 方法创建格式化字符串 format()功能 前言 string是C、java、VB等编程语言中的字符串&…

行星碰撞(力扣)栈 JAVA

给定一个整数数组 asteroids&#xff0c;表示在同一行的行星。 对于数组中的每一个元素&#xff0c;其绝对值表示行星的大小&#xff0c;正负表示行星的移动方向&#xff08;正表示向右移动&#xff0c;负表示向左移动&#xff09;。每一颗行星以相同的速度移动。 找出碰撞后剩…

unity进阶--xml的使用学习笔记

文章目录 xml实例解析方法一解析方法二 xml-path创建xml文档 xml实例 解析方法一 解析方法二 xml-path 创建xml文档

C++数据结构笔记(11)二叉树的#号创建法及计算叶子节点数

首先分享一段计算叶子节点数目的代码&#xff0c;如下图&#xff1a; 不难发现&#xff0c;上面的二叉树叶子节点数目为4。我们可以采用递归的方式&#xff0c;每当一个结点既没有左结点又没有右节点时&#xff0c;即可算为一个叶子结点。 int num0; //全局变量&#xff0c;代…

MyBatis-入门-快速入门程序

本次使用MyBatis框架是基于SpringBoot框架进行的&#xff0c;在IDEA中创建一个SpringBBot工程&#xff0c;根据自己的需求选择对应的依赖即可 快速入门 需求&#xff1a;使用MyBatis查询所有用户数据步骤&#xff1a; 准备工作&#xff08;创建Spring Boot工程、数据库user表…

【误差自适应跟踪方法AUV】自适应跟踪(EAT)方法研究(Matlab代码Simulin实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码、Simulink模型、文献 &#x1f4a5;1 概述 摘要&#xff1a;跟踪问题&#xff08;即如何遵循先前记忆的路径&#xff09;是移动机器人中最重要的问题之一。根据机器人状…

机器学习深度学习——线性回归的从零开始实现

虽然现在的深度学习框架几乎可以自动化实现下面的工作&#xff0c;但从零开始实现可以更了解工作原理&#xff0c;方便我们自定义模型、自定义层或自定义损失函数。 import random import torch from d2l import torch as d2l线性回归的从零开始实现 生成数据集读取数据集初始…

windows默认编码格式修改

1.命令提示符界面输入 chcp 936 对应 GBK 65001 对应 UTF-8 2.临时更改编码格式 chcp 936(或65001) 3.永久更改编码格式 依次开控制面板->时钟和区域->区域->管理->更改系统区域设置&#xff0c;然后按下图所示&#xff0c;勾选使用UTF-8语言支持。然后重启电脑。此…

防止连点..

1.连点js文件 let timer; letflag /*** 节流原理&#xff1a;在一定时间内&#xff0c;只能触发一次** param {Function} func 要执行的回调函数* param {Number} wait 延时的时间* param {Boolean} immediate 是否立即执行* return null*/ function throttle(func, wait 500…

【数字IC基础】竞争与冒险

竞争-冒险 1. 基本概念2. 冒险的分类3. 静态冒险产生的判断4. 毛刺的消除使用同步电路使用格雷码增加滤波电容增加冗余项&#xff0c;消除逻辑冒险引入选通脉冲 1. 基本概念 示例一&#xff1a; 如上图所示的这个电路&#xff0c;使用了两个逻辑门&#xff0c;一个非门和一个与…

Windows 找不到文件‘chrome‘。请确定文件名是否正确后,再试一次

爱像时间&#xff0c;永恒不变而又短暂&#xff1b;爱像流水&#xff0c;浩瀚壮阔却又普普通通。 Windows 找不到文件chrome。请确定文件名是否正确后&#xff0c;再试一次 如果 Windows 提示找不到文件 "chrome"&#xff0c;可能是由于以下几种原因导致的&#xff1…

机器学习深度学习——模型选择、欠拟合和过拟合

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——多层感知机的简洁实现 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有…

【GitOps系列】使用 ArgoCD 快速打造GitOps工作流

文章目录 ArgoCD简介ArgoCD安装访问ArgoCDGitOps 工作流总览创建 ArgoCD 应用检查 ArgoCD 同步状态访问应用 连接 GitOps 工作流体验 GitOps 工作流生产建议1&#xff09;修改默认密码2&#xff09;配置 Ingress 和 TLS3&#xff09;使用 Webhook 触发 ArgoCD4&#xff09;将源…

DoIP学习笔记系列:(二)VN5620 DoIP测试配置实践笔记

文章目录 1. 添加.cdd2. CAPL中调用接口发送DoIP请求3. “Ethernet Packet Builder”的妙用4. CANoe也可以做交互界面在进行测试前,先检查车载以太网硬件连线是否正确,需要注意连接两端的Master、Slave,100M、1000M等基本情况,在配置VN5620的时候就可以灵活处理了。成功安装…

数学建模-MATLAB三维作图

导出图片用无压缩tif会更清晰 帮助文档&#xff1a;doc 函数名 matlab代码导出为PDF 新建实时脚本或右键文件转换为实时脚本实时编辑器-全部运行-内嵌显示保存为PDF

【TypeScript】接口类型 Interfaces 的使用理解

导语&#xff1a; 什么是 类型接口&#xff1f; 在面向对象语言中&#xff0c;接口&#xff08;Interfaces&#xff09;是一个很重要的概念&#xff0c;它是对行为的抽象&#xff0c;而具体如何行动需要由类&#xff08;classes&#xff09;去实现&#xff08;implement&#x…

JVM-类加载

1.了解冯诺依曼计算机结构 1.1计算机处理数据过程 (1)提取阶段:由输入设备把原始数据或信息输入给计算机存储器存起来 (2)解码阶段:根据CPU的指令集架构(ISA)定义将数值解译为指令 (3)执行阶段:再由控制器把需要处理或计算的数据调入运算器 (4)最终阶段:由输出设备把最后运…

区间预测 | MATLAB实现基于QRF随机森林分位数回归时间序列区间预测模型

区间预测 | MATLAB实现基于QRF随机森林分位数回归时间序列区间预测模型 目录 区间预测 | MATLAB实现基于QRF随机森林分位数回归时间序列区间预测模型效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现基于QRF随机森林分位数回归时间序列区间预测模型&#xff1…

Dooring-Saas低代码技术详解

hello, 大家好, 我是徐小夕, 今天和大家分享一下基于 H5-Dooring零代码 开发的全新零代码搭建平台 Dooring-Saas 的技术架构和设计实现思路. 背景介绍 3年前我上线了第一版自研零代码引擎 H5-Dooring, 至今已迭代了 300 多个版本, 主要目的是快速且批量化的生产业务/营销过程中…