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

数据结构_ 二叉树线索化:从原理到手撕实现

目录

  • 概念
    • 为什么需要线索化?
    • 核心概念
    • 结构定义
  • 手撕中序线索化二叉树
    • 二叉树的定义
    • 如何构建线索二叉树(原理)
    • 代码实现及其原理
      • 手撕递归与线索化
      • 手撕线索化遍历
  • 总结

概念

二叉树线索化是一种对二叉树结构的优化技术,目的是利用二叉树中闲置的空指针(即左右孩子为空的指针),存储节点在某种遍历顺序(如中序、前序、后序)中的前驱或后继节点信息,从而实现二叉树的高效遍历(无需递归或栈)。

为什么需要线索化?

普通二叉树的遍历(如中序、前序)通常依赖递归或栈来记录节点的前驱 / 后继关系,但存在两个问题:
空指针浪费:对于一个有 n 个节点的二叉树,有 n+1 个空指针(每个节点有 2 个指针,共 2n 个,实际使用 n-1 个指向孩子,剩余 2n - (n-1) = n+1 个为空)。
遍历效率低:递归需要栈空间,迭代需要手动维护栈,且无法直接通过节点找到其前驱 / 后继。
线索化通过利用这些空指针存储 “线索”(指向前驱或后继的指针),解决了上述问题。

核心概念

线索:将二叉树中原本为空的左 / 右指针,改为指向该节点在某种遍历顺序中的前驱节点(左线索)或后继节点(右线索)。
标志位(tag):为了区分指针是指向孩子(真实指针)还是指向线索(前驱 / 后继),每个节点会增加两个标志位:
ltag:左标志位,0 表示左指针指向左孩子(真实指针),1 表示左指针是左线索(指向前驱)。
rtag:右标志位,0 表示右指针指向右孩子(真实指针),1 表示右指针是右线索(指向后继)。

结构定义

typedef struct Node {int val;          // 节点值int ltag, rtag;   // 0:真实孩子,1:线索struct Node *lchild, *rchild;  // 左/右指针(可能是孩子或线索)
} Node;

本篇我们详细的探讨中序线索化二叉树:按中序遍历(左→根→右)规则,为每个节点添加前驱(左线索)和后继(右线索)。

手撕中序线索化二叉树

通过二叉树文章可以知道,二叉树的基本操作,初始化一个结点,插入二叉树,销毁二叉树

二叉树的定义

这里就不再过多赘述了,需要注意的是,在线索化二叉树时,加入了ltag,rtag两个标志位,所以,在前中后序遍历的时候需要走真实路径,也就是 ltag = 0 , rtag = 0 的情况

所以在遍历的时候代码应该为:

//前序遍历
void pre_order(Node* root) {if (root == NULL)return;printf("%d ", root->val);if(root->ltag==0)pre_order(root->lchild);if (root->rtag == 0)pre_order(root->rchild);return;
}
//中序遍历
void mid_order(Node* root) {if (root == NULL)return;if (root->ltag == 0)mid_order(root->lchild);printf("%d ", root->val);if (root->rtag == 0)mid_order(root->rchild);return;
}
//后序遍历
void back_order(Node* root) {if (root == NULL)return;if (root->ltag == 0)back_order(root->lchild);if (root->rtag == 0)back_order(root->rchild);printf("%d ", root->val);return;
}

如何构建线索二叉树(原理)

在这里插入图片描述
我们以这个树结构为例子

在这里插入图片描述

根据概念:将二叉树中原本为空的左 / 右指针,改为指向该节点在某种遍历顺序中的前驱节点(左线索)或后继节点(右线索)。
分别站在每个结点上分析一下:
中序遍历:61 32 5 77 97 66 10 11 74 43 .

节点 61:

  • 左孩子为空 → 左线索指向 NULL(因为是中序遍历的第一个节点,无前驱),ltag=1。
  • 右孩子存在(32,真实子节点) → rtag=0,右指针仍指向 32。
    节点 32:
  • 左孩子为空 → 左线索指向前驱 61,ltag=1。
  • 右孩子存在(5,真实子节点) → rtag=0,右指针仍指向 5。
    节点 5:
  • 左孩子为空 → 左线索指向前驱 32,ltag=1。
  • 右孩子为空 → 右线索指向后继 77,rtag=1。
    节点 77:
  • 左孩子存在(61,真实子节点) → ltag=0,左指针仍指向 61。
  • 右孩子存在(66,真实子节点) → rtag=0,右指针仍指向 66。
    节点 97:
  • 左孩子为空 → 左线索指向前驱 77,ltag=1。
  • 右孩子为空 → 右线索指向后继 66,rtag=1。
    节点 66:
  • 左孩子存在(97,真实子节点) → ltag=0,左指针仍指向 97。
  • 右孩子存在(43,真实子节点) → rtag=0,右指针仍指向 43。
    节点 10:
  • 左孩子为空 → 左线索指向前驱 66(因为 10 的前一个节点是 66),ltag=1。
  • 右孩子为空 → 右线索指向后继 11,rtag=1。
    节点 11:
  • 左孩子存在(10,真实子节点) → ltag=0,左指针仍指向 10。
  • 右孩子存在(74,真实子节点) → rtag=0,右指针仍指向 74。
    节点 74:
  • 左孩子为空 → 左线索指向前驱 11,ltag=1。
  • 右孩子为空 → 右线索指向后继 43,rtag=1。
    节点 43:
  • 左孩子存在(11,真实子节点) → ltag=0,左指针仍指向 11。
  • 右孩子为空 → 右线索指向 NULL(因为是中序遍历的最后一个节点,无后继),rtag=1。

代码实现及其原理

在构建二叉树线索化时,最重要的就是build_midTreeThread()这个函数

Node* prenode = NULL,*midorder=NULL;
void build_midTreeThread(Node* root) {if (root == NULL) return;if (root->ltag == 0) build_midTreeThread(root->lchild);if (midorder == NULL) midorder = root;if (root->lchild == NULL) {root->lchild = prenode;root->ltag = 1;}if (prenode && prenode->rchild == NULL) {prenode->rchild = root;prenode->rtag = 1;}prenode = root;if (root->rtag == 0) build_midTreeThread(root->rchild);return;
}

手撕递归与线索化

这个函数设计的巧妙之处,在于记录前一个结点prenode(midorder后面会讲到,这里理解为这个二叉树第一次遍历到的点),这样一旦发现当前结点左孩子为NULL就可以直接指向pernode,来完成前驱这一步,但是这里难得点还是在于递归所以,这里着重带着大家分析一下这里是如何递归的(看着上面的图食用更佳哦)。
进入构建函数:
在这里插入图片描述
可以看到,初始化的prenode和第一个中序遍历得到的节点都为NULL,第一个结点的val=77,准备递归
在这里插入图片描述代码走到这一步,代表现在进入了 77的左子树,的最左边的结点
在这里插入图片描述
也就是 61 ,可以看到,跟上面的图是一致的,61这个结点左孩子为NULL,右孩子为右子树,这里我们要开始构建第一个线索化,也就是将 61 的前驱(lchild)记为NULL
在这里插入图片描述
代码进入到这一步,将prenode(NULL)的值赋给61的左孩子,将ltag更改为1,代表这是一条线索化
在这里插入图片描述
prenode为NULL,不执行此语句
在这里插入图片描述

最后更新prenode为 61
在这里插入图片描述
当代码执行到这一步时,代表要进入 61 的右子树

在这里插入图片描述
又开始递归进入左子树,由于32没有左子树,所以进入线索化
在这里插入图片描述
在这里插入图片描述
将root(32)做线索化处理,左孩子指向prenode(61)
由于prenode(61)存在右子树,所以不做线索化处理,进入root(32)的右子树
在这里插入图片描述
此时root=5做线索化处理,左孩子指向prenode(32)
在这里插入图片描述
走到 5 时代表 32 的右子树走完了,回到32,继续回到61,最终回到77
在这里插入图片描述
在这里插入图片描述
由此可见,线索化成功
当结点回到77的时候,代表77的左子树已经全部线索化完毕,但是需要注意的是,此时prenode还在5这个结点,最终 5 指向此时root(77),也就是5的后继
在这里插入图片描述
在这里插入图片描述
更新完毕,准备进入77右子树的递归
在这里插入图片描述
与 77 的左子树相同,同样会先递归到最左边的元素,也就是77中序遍历第一次在右子树中访问到的元素 97
在这里插入图片描述
在这里插入图片描述
由于root(97)的左孩子为NULL,将其线索化指向prenode(77)
回溯到结点 66
在这里插入图片描述
在这里插入图片描述
prenode的右孩子也为NULL
在这里插入图片描述
对其线索化,指向root(66)
在这里插入图片描述
成功线索化97结点的前驱与后继

进入66的右子树,依旧重复,先递归到66右子树的最左边(最先访问到的值)10
在这里插入图片描述
在这里插入图片描述
左孩子为NULL,指向prenode(66)
回溯root=11,prenode(10)
在这里插入图片描述
prenode(10)的右孩子为NULL,对其线索化指向root(11)

递归进入11的右子树 74
在这里插入图片描述

74的左孩子为NULL,对其线索化指向prenode(11)
最终回到43
在这里插入图片描述
prenode(74)在这里插入图片描述
右孩子为NULL,对其线索化指向root(43)
在这里插入图片描述
最后更新prenode为43,回溯到77,递归完成
在这里插入图片描述
最后再封装一个函数,将43的右孩子指向NULL即可:

void __build_midTreeThread(Node * root) {build_midTreeThread(root);prenode->rchild = NULL;prenode->rtag = 1;return;
}

手撕线索化遍历

Node* node = midorder;//第一个元素

这里我们就用上了之前设置的midorder这个变量,记录了第一个线索化的值,我们从这个值开始,根据线索对这个二叉树进行遍历
这里的遍历和遍历链表很类似,都是获取到下一个结点,依次遍历,那么我们的重点就是在这个getNext()函数了

	while (node) {printf("%d ", node->val);node = getNext(node);}
Node* getNext(Node* node) {if (node->rtag == 1) {return node->rchild;}node = node->rchild;while (node->lchild && node->ltag == 0) node = node->lchild;return node;
}

在这里插入图片描述
在这里插入图片描述
当一个结点拥有线索化(l/rtag=1)时,直接返回其后的线索
如果没有线索化,根据中序遍历的特性: 左 根 右,当前结点的后继结点必然为右子树中的最左侧的结点
得出:

	node = node->rchild;  // 进入右子树// 循环找到右子树中最左侧的节点(左指针为真实子树时继续向左)while (node != NULL && node->ltag == 0 && node->lchild != NULL) {node = node->lchild;}

例如,61 没有rchild线索化,那么就进61的右子树查看,node此时为32,32没有左孩子,所以循环直接跳过,返回32
在这里插入图片描述
在这里插入图片描述
依次重复,直至到 5 这个结点,进入getNext(5),此时由于5有rchild的后继线索,直接返回77

77进入getNext();获取其右子树 66 ,一直遍历66的左子树部分,得到97,97的拥有rchild线索,得到66,没有线索,得到其右子树43,进入43,再继续遍历43的左子树部分,得到10,10拥有rchild线索,得到11,11没有线索,得到11的右子树74,没有左子树,直接返回,74拥有线索化43,最后直接得到43,至此,线索化遍历完毕
在这里插入图片描述
利用递归和线索化遍历结果相同,中序线索化完毕(遍历时无需递归或栈)

总结

中序线索化二叉树通过利用空指针存储遍历顺序信息,实现了:

  • 空间优化:利用n+1个空指针
  • 遍历效率:O(n)时间复杂度,O(1)空间复杂度
  • 无需递归栈:线性遍历整个二叉树

适用于需要频繁进行中序遍历且对空间效率要求较高的场景。

http://www.dtcms.com/a/420113.html

相关文章:

  • 分享一个知识工程师单体智能体的简单提示词
  • 南宁伯才网络建站如何WordPress一键采集插件
  • 免费三网合一网站系统网站建设介绍书
  • 网站开发公司起名10分钟快速建网站
  • flink批处理-时间和窗口
  • 无锡有什么网站怎样免费注册个人网网址
  • SLAM | SLAM中松耦合与紧耦合技术对比分析
  • xtuoj 方程组
  • 重庆网站设计建设东莞微联建站
  • 北京医疗网站建设公司排名网站建设开票项目是什么
  • C#——方法的定义、调用与调试
  • 开源的容器化平台:Docker入门与核心概念解析
  • 手机网站价格小广告网页
  • 晋城市建设局网站国内编程培训机构排名
  • 08 线性回归 + 基础优化算法【动手学深度学习v2】
  • 创新为擎,绿色为基:新天力科技引领食品容器行业高质量发展新征程
  • 从 GDPR 到 ROI:一次评透海外影音数据抓取的合规与性价比
  • 商务网站规划与建设中江建设银行网站
  • 关于做书的网站专门做项目代理的网站
  • 【Vue3】v-dialog 中使用 execCommand(‘copy‘) 复制文本失效的原因与解决方案
  • lesson70:jQuery Ajax完全指南:从基础到4.0新特性及现代替代方案引言:jQuery Ajax的时代价值与演进
  • 郑州网站建设 李浩大气集团企业网站模板
  • Redis高级特性:持久化机制
  • 哈尔滨模板建站哪个品牌好网站怎么做交易
  • 城建设投资公司网站潍坊路通工程建设有限公司网站
  • 个人工作室网站如何把字体安装在wordpress
  • 轴状态读取(枚举变量直接赋值给整型变量)
  • 做哪方面的网站好呢深圳ppt设计制作公司
  • 做ppt找图片在哪个网站电商运营工作很难做吗
  • qqqqqqq