数据结构——二叉树线索化遍历(前中后序遍历)

二叉树线索化

线索化概念:

为什么要转换为线索化

        二叉树线索化是一种将普通二叉树转换为具有特殊线索(指向前驱和后继节点)的二叉树的过程。这种线索化的目的是为了提高对二叉树的遍历效率,特别是在不使用递归或栈的情况下进行遍历。

        将二叉树线索化的主要目的是为了提高对二叉树的遍历效率以及节省存储空间。线索化使得在不使用递归或栈的情况下可以更快速地进行遍历,特别是在特定顺序的遍历时,如前序、中序或后序遍历。  

  1. 提高遍历效率:线索化后,可以在常量时间内找到节点的前驱和后继节点,从而实现更高效的遍历。这对于需要频繁遍历大型二叉树或需要在树的中间部分执行插入和删除操作时特别有用。

  2. 无需递归或栈:线索化的二叉树允许你在遍历时省去递归或栈的开销,因为你可以沿着线索直接访问节点的前驱和后继,从而降低了内存和时间复杂度。

  3. 节省存储空间:线索化可以用较少的额外存储空间来实现。通常,只需为每个节点添加一个或两个指针来存储线索信息,而不需要额外的数据结构(如堆栈)来辅助遍历。

  4. 支持双向遍历:线索化的二叉树可以支持双向遍历,即可以在给定节点的前向和后向方向上遍历树。这在某些应用中很有用,例如双向链表的操作。

  5. 节省计算资源:在某些特定的应用场景中,通过线索化可以避免重复计算,因为可以直接访问前驱和后继节点,而无需再次搜索或遍历

树的遍历

        先给定一棵树,然后对他进行每种遍历:

        前序遍历:

        前序遍历,就是先遍历根节点在遍历左子树,在遍历右子树;

        他的顺序就是:根节点->左子树->右子树

        根据递归先遍历了根节点,然后递归左子树,回溯后在进行遍历右子树的一个过程;

        例如上图开始前序遍历:

        遍历根节点50,然后递归左子树,34,现在把34看为根节点继续递归左子树,28,然后把28看作根节点,继续遍历左子树,19,然后把19看为根节点继续遍历,然后左子树为空,开始回溯,回溯到19,遍历右子树也为空,继续回溯到结点28,遍历右子树,31;然后通过这种思想一直进行遍历最总遍历完整棵子树;

        前序遍历结果:50,34,28,19,31,41,81,72,95

        中序遍历:

        顺序是:左子树->根节点->右子树

        其实在理解了前序遍历后,中序遍历也差不多的,刚才是先记录了根节点,现在开始,先一直递归遍历左子树,递归到没有左子树的时开始记录,比如上图递归到19,然后回溯,28,遍历右子树31,回溯34,遍历右子树41,回溯50,遍历右子树;然后最终的结果就是:

        19,28,31,34,41,50,72,81,95;

        中序遍历就是,找到子树的最左边的那个结点,然后回溯到它的父节点,然后遍历他父节点的右子树,然后到右子树中又去找它的最左的结点,这样一直经过这样的操作,最终完成中序遍历。

        后序遍历:

        顺序是:左子树->右子树->根节点

        先是左子树,那就先找到左子树中的最左边的结点,19,然后回溯,遍历右子树,然后再右子树中找最左边的结点,31,回溯28,然后在回溯;这样的一个过程;

        最终结果就是:

        19,31,28,41,34,72,95,81,50

        后序遍历,看着比中序和后序遍历难理解,其实只要懂得了递归回溯的那个过程,就思路回非常的清晰;然后大概的过程就是先遍历左子树,找到左子树中最左边的结点,回溯,然后遍历右子树,遍历右子树的过程也是先遍历右子树中的左子树,然后再进行遍历右子树的右子树,最后来遍历他们的根节点;

        3种遍历的代码实现:  


void pre_orderNode(Node *root) {前序if (!root) return ;printf("%d ", root->data);//先输出根节点pre_orderNode(root->lchild);//遍历左子树pre_orderNode(root->rchild);//遍历右子树return ;
}void in_orderNode(Node *root) {中序if (!root) return ;in_orderNode(root->lchild);//先遍历左子树printf("%d ", root->data);//在打印根节点的值in_orderNode(root->rchild);//在遍历右子树return ;
}void post_orderNode(Node *root) {后序if (!root) return ;post_orderNode(root->lchild);//先遍历左子树post_orderNode(root->rchild);//在遍历右子树printf("%d ", root->data);//最终打印根节点值return ;
}

        可以去理解一下代码,尝试在纸上或者脑子里执行以下代码,模拟运行以下;

二叉树前序线索化:

        

        如图上图一个简单的二叉树,现在是将这个二叉树进行前序线索化,那么前序遍历的顺序是根节点->左子树->右子树;

        那么根节点就应该在条线的头部,然后再去遍历左子树和右子树,那么这个数的前序遍历结果是50,34,79;

        

        那么就可以将二叉树样来看,50是34的前驱,79是34的后继,前驱就是在遍历在他前面的,后继就是在他后面遍历的;

        现在我们将每个结点的指向两个子孩子指针改变为指向他的前驱和后继:

        通过这样的转换,那这棵树就转换为一条双向链表了:

        其中会发下50和79的会右一颗子树指向NULL,那么就需要用一个变量来记录,他是否右前驱或者后继;

        就是这样的一个转换,也证明了概念种的节省空间,不用递归遍历,可以双向遍历,提高了遍历效率;

        下面是代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>#define NORMAL 0
#define THRENA 1 //这里左儿子记录前驱,右儿子记录后继
typedef struct Node {int val, ltag, rtag;//val结点的值,ltag记录是否有前驱,rtag记录是否有后继struct Node *lchild, *rchild;
} Node;Node *getNewNode(int val) {//获取新的结点Node *root = (Node *)malloc(sizeof(Node));root->lchild = root->rchild = NULL;root->rtag = root->ltag = NORMAL;//NORMAL表示还未有前驱或后继root->val = val;return root;
}Node *insert(Node *root, int val) {//添加结点,组成普通的二叉树if (!root) return getNewNode(val);if (root->val == val) return root;if (root->val > val) root->lchild = insert(root->lchild, val);else root->rchild = insert(root->rchild, val);return root;
}void build_Thread(Node *root) {//建立线索化if (!root) return ; static Node *pre = NULL;//使用静态变量使得pre值不随程序递归而改变Node *left = root->lchild;//记录当前结点左右儿子 Node *right = root->rchild;if (root->ltag == NORMAL) {//当前结点没有前驱结点root->lchild = pre;//给当前结点赋值前驱结点root->ltag = THRENA;//标记有前驱结点}if (pre && pre->rtag == NORMAL) {//如果它的前驱结点没有后继,并且前驱结点不为NULLpre->rchild = root;//将前驱结点的后继附上当前结点pre->rtag = THRENA;//标记前驱结点有后继了}pre = root;//pre等于当前递归的结点build_Thread(left);//递归左子树build_Thread(right);//在递归右子树return ;
}void output(Node *root) {//遍历线索化二叉树if (!root) return ;Node *p = root;while (p) {printf("%d ", p->val);if (p->rtag == THRENA) p = p->rchild;//说明当前结点有后继直接往右节点也就是后继结点继续遍历else if (p->rtag == NORMAL) break;//如果当前结点没有后继结束遍历 }return ;
}void preface(Node *root) {//普通前序递归遍历if (!root) return ;printf("%d ", root->val);if (root->ltag == NORMAL) preface(root->lchild);if (root->rtag == NORMAL) preface(root->rchild);return ;
}void clearNode(Node *root) {//回收空间if (!root) return ;if (root->ltag == NORMAL) clearNode(root->lchild);if (root->rtag == NORMAL) clearNode(root->rchild);free(root);return ;
}int main() {srand(time(0));//获取当期计算时间,获取随机数Node *root = NULL;for (int i = 0; i < 10; i++) {int val = rand() % 100;root = insert(root, val);}preface(root);//先打印普通前序遍历putchar(10);//换行build_Thread(root);//建立线索化output(root);//输出前序线索化putchar(10);clearNode(root);return 0;
}

二叉树中序线索化:

       

        中序遍历顺序:左子树->根节点->右子树

        那么结果借是34,50,79

        转换线索化,那么34就是50的前驱,50就是34的后继;和前序遍历的是一样的;

        直接来看代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define NORMAL 0 //表示没有
#define THRENA 1 //表示有
//这里左儿子记录前驱,右儿子记录后继
typedef struct Node {int val, ltag, rtag;///val结点的值,ltag记录是否有前驱,rtag记录是否有后继struct Node *lchild, *rchild; 
} Node;Node *getNewNode(int val) {///获取新的结点Node *p = (Node *)malloc(sizeof(Node));p->val = val;p->lchild = p->rchild = NULL;p->ltag = p->rtag = NORMAL;//NORMAL表示还未有前驱或后继return p;
}Node *insert(Node *root, int val) {//添加结点,组成普通的二叉树if (!root) return getNewNode(val);if (root->val == val) return root;if (root->val > val) root->lchild = insert(root->lchild, val);else root->rchild = insert(root->rchild, val);return root;
}void in_order(Node *root) {//普通递归中序遍历if (!root) return ;if (root->ltag == NORMAL) in_order(root->lchild);printf("%d ", root->val);if (root->rtag == NORMAL) in_order(root->rchild);return ;
}void build_Thread(Node *root) {//建立线索化if (!root) return ;static Node *pre = NULL;//使用静态变量使得pre值不随函数递归过程改变而改变build_Thread(root->lchild);//由于是中序遍历,先递归到最左结点//中间过程就是想当于根节点if (root->ltag = NORMAL) {root->lchild = pre;root->ltag = THRENA;}if (pre && pre->rchild == NORMAL) {pre->rchild = root; pre->rtag = THRENA;}pre = root;build_Thread(root->rchild);//然后再遍历右子树return ;
}Node *most_left(Node *root) {//找到最左结点Node *temp = root;while (temp && temp->ltag == NORMAL && temp->lchild != NULL) temp = temp->lchild;return temp;
}void output(Node *root) {if (!root) ;Node *p = most_left(root);//从最左结点开始遍历while (p) {printf("%d ", p->val);if (p->rtag == THRENA) p = p->rchild;//如果后继存在进行遍历后继else p = most_left(p->rchild);//找到右子树的最左结点继续遍历}return ;
}void clear(Node *root) {if (!root) return ;if (root->ltag == NORMAL) clear(root->lchild);if (root->rtag == NORMAL) clear(root->rchild);free(root);return ;
}int main() {srand(time(0));    Node *root = NULL;for (int i = 0; i < 10; i++) {int val = rand() % 100;root = insert(root, val);}in_order(root);putchar(10);build_Thread(root);output(root);putchar(10);clear(root);return 0;
}

        二叉树后序线索化:

        直接上代码演示,如果前面两种你都弄懂了那么,后序理解起来也非常容易:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>#define NORMAL 0
#define THRENA 1 typedef struct Node {int val, ltag, rtag;struct Node *lchild, *rchild;
} Node;Node *getNewNode(int val) {Node *root = (Node *)malloc(sizeof(Node));root->lchild = root->rchild = NULL;root->rtag = root->ltag = NORMAL;root->val = val;return root;
}Node *insert(Node *root, int val) {if (!root) return getNewNode(val);if (root->val == val) return root;if (root->val > val) root->lchild = insert(root->lchild, val);else root->rchild = insert(root->rchild, val);return root;
}void build_Thread(Node *root) {if (!root) return ; static Node *pre = NULL;build_Thread(root->lchild);build_Thread(root->rchild);if (root->ltag == NORMAL) {root->lchild = pre;root->ltag = THRENA;}if (pre && root->rtag == NORMAL) {pre->rchild = root;pre->rtag = THRENA;}pre = root;return ;
}Node *most_left(Node *root) {while (root && root->ltag == THRENA && root->lchild) root = root->lchild;return root;
}void output(Node *root) {if (!root) return ;Node *p = most_left(root);while (p) {printf("%d ", p->val);if (p->rtag == THRENA) p = p->rchild;  else if (p->rtag == NORMAL) break;}return ;
}void back_order(Node *root) {if (!root) return ;if (root->ltag == NORMAL) back_order(root->lchild);if (root->rtag == NORMAL) back_order(root->rchild);printf("%d ", root->val);return ;
}void preface(Node *root) {if (!root) return ;printf("%d(", root->val);if (root->ltag == NORMAL) preface(root->lchild);printf(",");if (root->rtag == NORMAL) preface(root->rchild);printf(")");return ;
}void clearNode(Node *root) {if (!root) return ;if (root->ltag == NORMAL) clearNode(root->lchild);if (root->rtag == NORMAL) clearNode(root->rchild);free(root);return ;
}int main() {srand(time(0)); Node *root = NULL;for (int i = 0; i < 10; i++) {int val = rand() % 100;root = insert(root, val);}preface(root);putchar(10);back_order(root);putchar(10);build_Thread(root);output(root);putchar(10);clearNode(root);return 0;
}

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

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

相关文章

Docker部署(5)——使用docker run命令部署运行jar项目

对于一些简单的单体项目&#xff0c;可以使用 docker run 命令可以直接在命令行中运行容器&#xff0c;无需事先构建镜像。这相较于之前使用的 dockerfile 文件来运行部署项目相当于是另外一种简单的部署方法&#xff0c;关于之前使用dockerfile 文件来运行部署这种方法&#x…

linux下安装qt、qt触摸屏校准tslib

linux下安装qt 在 Linux 系统下安装 Qt&#xff0c;可以通过以下步骤进行操作&#xff1a;1. 下载 Qt 安装包&#xff1a;首先&#xff0c;你需要从 Qt 官方网站&#xff08;https://www.qt.io/&#xff09;下载适用于 Linux 的 Qt 安装包。选择与你的系统和需求相匹配的版本&…

chrome extension无法获取window对象

背景见上一篇博客修改网页内容的方法 上一篇博客之后&#xff0c;我要修改的网页有一个新改版&#xff0c;然后有个数据存在了window中&#xff0c;我直接在js中使用window.xxx发现无法获取。所以有本文。 https://juejin.cn/post/7145749643316428830 https://onelinerhub.com…

网约车围城:百万司机涌入,狼多肉少

狼多肉少&#xff0c;网约车围城已经成为了一个不争的事实。虽然市场竞争越来越激烈&#xff0c;但机会还是有的&#xff0c;司机们也应该不断提高自身的服务质量和素质&#xff0c;以满足消费者的需求&#xff0c;获取更多的订单和收益。 网约车市场一直以来都是人们生活中不可…

为什么要分库分表?

分析&回答 什么是分库分表&#xff1f; 分库&#xff1a;从单个数据库拆分成多个数据库的过程&#xff0c;将数据散落在多个数据库中。分表&#xff1a;从单张表拆分成多张表的过程&#xff0c;将数据散落在多张表内。 为什么要分库分表&#xff1f; 主要为了提升性能、…

l8-d8 TCP并发实现

一、TCP多进程并发 1.地址快速重用 先退出服务端&#xff0c;后退出客户端&#xff0c;则服务端会出现以下错误&#xff1a; 地址仍在使用中 解决方法&#xff1a; /*地址快速重用*/ int flag1,len sizeof (int); if ( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &a…

B-Tree 索引和 Hash 索引的对比

分析&回答 B-Tree 索引的特点 B-tree 索引可以用于使用 , >, >, <, < 或者 BETWEEN 运算符的列比较。如果 LIKE 的参数是一个没有以通配符起始的常量字符串的话也可以使用这种索引。 有时&#xff0c;即使有索引可以使用&#xff0c;MySQL 也不使用任何索引。…

【漏洞复现】E-office文件包含漏洞

漏洞描述 Weaver E-Office是中国泛微科技(Weaver)公司的一个协同办公系统。泛微 E-Office 是一款标准化的协同 OA 办公软件,实行通用化产品设计,充分贴合企业管理需求,本着简洁易用、高效智能的原则,为企业快速打造移动化、无纸化、数字化的办公平台。 该漏洞是由于存在…

巨人互动|游戏出海游戏出海效果怎样?

游戏出海是指将原本面向国内市场的游戏产品进行调整和优化&#xff0c;以适应海外市场的需求&#xff0c;并进行推广和销售。下面小编讲讲关于游戏出海对于游戏效果的影响的一些讨论点。 1、市场扩大 通过游戏出海&#xff0c;可以将游戏产品的目标受众从国内扩展到全球范围内…

Zebec Protocol 成非洲利比亚展会合作伙伴,并将向第三世界国家布局

在 9 月 6 日&#xff0c;The Digital Asset Summit ’23&#xff08;利比亚大会&#xff09;在尼日利亚首度阿布贾的 NAF 会议中心举办&#xff0c;该会议对 Web3 领域在非洲地区的发展进行了探索&#xff0c;旨在推动非洲地区区块链产业的进一步发展&#xff0c;据悉该会议室…

CSS 设置渐变背景 CSS 设置渐变边框

一、css渐变背景添加透明度opacity css渐变背景经常会在项目开发中遇到&#xff0c;此时UI如果给出的是单一的渐变背景&#xff08;没有背景透明度&#xff09;&#xff0c;这个我们会很快的写出代码&#xff0c;如下: <div class"btn">这是一个按钮</div&…

神策数据发布汽车行业 CJO 解决方案,打造客户旅程全新体验

最近&#xff0c;围绕数字化客户经营&#xff0c;神策数据基于“客户旅程编排&#xff08;Customer Journey Orchestration&#xff0c;简称 CJO&#xff09;”理念&#xff0c;发布汽车行业全新解决方案&#xff0c;通过全渠道打通给客户带来一致的、个性化的体验&#xff0c;…

【HTML专栏1】语法规范、基础结构标签

本文属于HTML/CSS专栏文章&#xff0c;适合WEB前端开发入门学习&#xff0c;详细介绍HTML/CSS如果使用&#xff0c;如果对你有所帮助请一键三连支持&#xff0c;对博主系列文章感兴趣点击下方专栏了解详细。 博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;HTML/CS…

第4章:网络层

文章目录 一、概述和功能2.SDN二、转发1.IP数据报(1)IP数据报的首部字段(2)IP数据报的分片2.IPv4地址:<网络号>,<主机号>3.IP编址 (三个历史阶段)(1)分类IP地址①特殊IP地址②私有IP地址③网络地址转换NAT:导致IP地址变化MAC地址、IP地址变化问题(2)子网划分与子…

go logger 不侵入业务代码 用slog 替换 zap 并实现 callerSkip

快速体验 以下是 项目中 已经用slog替换 zap 后的 logger 使用方法,与替换前使用方式相同,无任何感知 package mainimport "github.com/webws/go-moda/logger"func main() {// 格式化打印 {"time":"2023-09-08T01:25:21.31346308:00","le…

滴滴笔试——算式转移

题目&#xff1a;给出一个仅包含加减乘除四种运算符的算式(不含括号)&#xff0c;如12*3/4&#xff0c;在保持运算符顺序不变的情况下&#xff0c;现在你可以进行若干次如下操作&#xff1a;如果交换相邻的两个数&#xff0c;表达式值不变&#xff0c;那么你就可以交换这两个数…

Ceph入门到精通-生产日志级别设置

Ceph 子系统及其日志记录级别的信息。 了解 Ceph 子系统及其日志记录级别 Ceph 由多个子系统组成&#xff1a; 每个子系统都有其日志记录级别&#xff1a; 默认情况下存储在 /var/log/ceph/ 目录中的输出日志&#xff08;日志级别&#xff09;存储在内存缓存中的日志&#…

无涯教程-JavaScript - DEC2HEX函数

描述 DEC2HEX函数将十进制数转换为十六进制。 语法 DEC2HEX (number, [places])争论 Argument描述Required/Optionalnumber 要转换的十进制整数。 如果number为负数,则将忽略位数,并且DEC2HEX返回10个字符(40位)的十六进制数字,其中最高有效位是符号位。其余的39位是幅度位…

Laravel 模型的关联写入多对多的关联写入 ⑩③

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; THINK PHP &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

读高性能MySQL(第4版)笔记04_操作系统和硬件优化

1. 从软件本身和它运行的典型工作负载来看&#xff0c;MySQL通常也更适合运行在廉价硬件上 2. 基本资源 2.1. CPU 2.2. 内存 2.3. 磁盘 2.4. 瓶颈 2.5. 网络资源 3. CPU 3.1. 最常见的瓶颈是CPU耗尽 3.2. 检查CPU使用率来确定工作负载是否受CPU限制 3.3. 低延迟&…