news 2026/2/6 8:49:36

数据结构之线索二叉树

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据结构之线索二叉树

一文读懂线索二叉树的原理与用法

前言须知

先了解以下概念,再来学习线索二叉树⬇️

  • 前驱结点:二叉树里的前驱结点,是某一种遍历顺序下上一个被遍历的结点。不同的遍历顺序(中序、前序、后序),同一个节点的前驱节点可能完全不同。
  • 后继结点:二叉树里的前驱结点,是某一种遍历顺序下下一个被遍历的结点。不同的遍历顺序(中序、前序、后序),同一个节点的前驱节点可能完全不同。

而所谓的中序前驱中序后继,只不过是按照中序遍历的顺序,上一个被访问的结点和下一个被访问的结点,后序前驱和后序后继,以及先序前驱和先序后继也是一样的,按照先序/后序的遍历顺序,上一个被访问的结点和下一个被访问的结点。
注:本文只会用到一个二叉树结构,以下图所示⬇️

此二叉树的中序序列是:D(1) -> G(2) -> B(3) -> E(4) -> A(5) -> F(6) -> C(7)

括号内的数字表示遍历顺序,D(1):表示D结点是第一个遍历的结点

结点B的中序前驱就是G ,中序后继就是E,可以发现除了第一个被遍历的结点没有前驱结点,和最后一个被遍历的结点没有后继结点外,其他结点都有且只有一个前驱和后继结点。

思考两个问题


问题1:找到q结点的中序前驱和中序后继?

算法实现:

找中序前驱:从根结点出发,中序递归遍历该二叉树,若当前访问的结点p如果与q相等,pre就是G的中序前驱,反之不相等,pre = p让pre 指向正在访问的结点p。

找中序后继:从根结点出发,中序递归该二叉树,若pre == q,则p就是q的中序后继,反之,pre = p,让pre 指向正在访问的结点p。
代码实现:

voidfind_pre_in_order(BinaryTree p,BinaryTreeNode*q,BinaryTreeNode*&pre,BinaryTreeNode*&final){if(p!=NULL){find_pre_in_order(p->lchild,q,pre,final);if(p==q)// 若当前访问的p结点与目标结点相等final=pre;// final 指向 q 的前驱elsepre=p;// pre 指向当前结点 p的前驱find_pre_in_order(p->rchlid,q,pre,final);}}// 对外接口:找指定节点q的中序前驱BinaryTreeNode*FindPreNode(BinaryTree T,BinaryTreeNode*q){/* T : 查找范围是T这棵二叉树 q : 目标结点 */BinaryTreeNode*final=NULL;BinaryTreeNode*pre=NULL;find_pre_in_order(T,q,pre,final);returnfinal;}voidfind_post_in_order(BinaryTree p,BinaryTreeNode*q,BinaryTreeNode*&pre,BinaryTreeNode*&final){if(p!=NULL){find_post_in_order(p->lchild,q,pre,final);if(pre==q)// 若当前访问的p结点与目标结点相等final=p;// final 指向 q 的后继\ elsepre=p;// pre 指向当前结点 p的前驱find_post_in_order(p->rchlid,q,pre,final);}}// 对外接口:找指定节点q的中序后继BinaryTreeNode*FindPostNode(BinaryTree T,BinaryTreeNode*q){/* T : 查找范围是T这棵二叉树 q : 目标结点 */intflag=1;BinaryTreeNode*final=NULL;BinaryTreeNode*pre=NULL;find_post_in_order(T,q,pre,final,&flag);returnfinal;}


问题2:从G结点开始,继续往后中序遍历?

算法实现:G结点只保存了其左孩子和右孩子的地址,没有办法从G结点开始,继续向下遍历,只能从根结点开始找G结点不断地找后继结点,直到找到最后一个遍历的结点为止。

这个算法的时间复杂度是O(N^2)

代码实现:

// FindPostNode()内部调用voidfind_post_in_order(BinaryTree p,BinaryTreeNode*q,BinaryTreeNode*&pre,BinaryTreeNode*&final);// 对外接口:找指定节点q的中序后继BinaryTreeNode*FindPostNode(BinaryTree T,BinaryTreeNode*q);// 打印结点数据voidvisit(BinaryTreeNode*p);// 中序遍历中序线索二叉树voidInOrder(BinaryTree T){if(T!=NULL){BinaryTreeNode*first=T;// 第一个被访问的结点while(first->lchild!=NULL)first=first->lchild;for(BinaryTreeNode*p=first;p!=NULL,FindPostNode(T,p)){visit(p);}}}

接下来引入线索二叉树来有效的解决上面的问题

线索二叉树

线索二叉树是对普通二叉树的一种优化存储结构,核心目的是利用二叉树中空闲的指针域(空链域),存储节点在某种遍历序列下的前驱后继节点地址,从而避免在遍历二叉树时使用栈或递归,提升遍历效率。

1. 普通二叉树的指针浪费问题

在一棵有 n 个节点的二叉树中,每个节点有2 个指针域(左孩子lchild、右孩子rchild),总共有 2n 个指针域。

但二叉树的边数只有 n−1 条(除了根节点,每个节点都有一个父节点),因此空闲的指针域数量为:

2n−(n−1)=n+1

这些空闲指针域被白白浪费,线索二叉树就是把这些空闲指针改造为线索

2. 三种线索二叉树

  1. 先序线索化: 按照先序遍历序列的顺序,将普通二叉树的空链域指向当前结点前驱和后继。
  2. 中序线索化: 按照中序遍历序列的顺序,将普通二叉树的空链域指向当前结点前驱和后继
  3. 后序线索化:按照后序遍历序列的顺序,将普通二叉树的空链域指向当前结点前驱和后继。

普通二叉树—》线索化 -》 线索二叉树

普通二叉树—》中序线索化 -》 中序线索二叉树。

3. 线索二叉树的存储结构

#defineElemTypechar// 线索二叉树结点的数据域类型typedefstructThreadBinaryTreeNode{ElemType data;structThreadBinaryTreeNode*lchild,rchild;intltag,rtag;// 标志位,标志左右孩子指针指向的是前驱和后继,还是左右孩子结点}*ThreadBTree,ThreadBTreeNode;/* 若ltag==1,表示lchild指向的是前驱 若ltag==0, 表示lchild指向的是左孩子 若rtag==1,表示rchild指向的是后继 若rtag==0, 表示rchild指向的是左孩子 */

4. 手画线索二叉树

此处,只举例手画中序线索二叉树,后序和先序的步骤类似

  1. 得到二叉树的中序遍历序列:D(1) -> G(2) -> B(3) -> E(4) -> A(5) -> F(6) -> C(7)
  2. 在每个结点的旁边写上被遍历的顺序。
  3. 将所有结点的空链域,用虚线指向该结点的前驱和后继。

手画结果:

5. 代码实现线索化

中序线索化代码实现:

voidin_thread(ThreadBTree P,ThreaBTreeNode*&pre){if(P!=NULL){in_thread(P->lchild,pre);if(P->lchild==NULL){P->lchilPd=pre;P->ltag=1;}if(pre!=NULL&&pre->rchild==NULL){pre->rchild=P;pre->rtag=1;}pre=P;in_thread(P->rchild,pre);}}// 构建中序线索二叉树voidCreateInThreadBTree(ThreadBTree T){ThreadBTreeNode*pre=NULL;in_thread(T,pre);if(pre->rchild==NULL)// 处理最后一个被访问的结点(注:可以去掉if直接修改标志位,中序遍历最后一个结点的右孩子一定非空)pre->rtag=1;}

先序线索化代码实现

voidpre_thread(ThreadBTree P,ThreadBTreeNode*&pre){if(P!=NULL){if(P->lchild==NULL){P->lchild=pre;P->ltag=1;}if(pre!=NULL&&pre->rchild==NULL){pre->rchild=P;pre->rtag=1;}pre=p;pre_thread(P->lchild,pre);pre_thread(P->rchild,pre);}}// 构建先序线索二叉树voidCreatePreThreadBTree(ThreadBTree T){ThreadBTreeNode*pre=NULL;pre_thread(T,pre);if(pre->rchild==NULL){// 处理最后一个被访问的结点(注:可以去掉if直接修改标志位,线序遍历最后一个结点的右孩子一定非空)pre->rtag=1;}}

后序线索化代码实现:

voidpost_thread(ThreadBTree P,ThreadBTreeNode*&pre){if(P!=NULL){pre_thread(P->lchild,pre);pre_thread(P->rchild,pre);if(P->lchild==NULL){P->lchild=pre;P->ltag=1;}if(pre!=NULL&&pre->rchild==NULL){pre->rchild=P;pre->rtag=1;}pre=p;}}// 构建后序线索二叉树voidCreatePostThreadBTree(ThreadBTree T){ThreadBTreeNode*pre=NULL;post_thread(T,pre);if(pre->rchild==NULL){pre->rtag=1;}}

中序线索二叉树的前驱和后继

问题1:找到q结点的中序前驱和中序后继?

问题2:从G结点开始,继续往后中序遍历?

前面提到的两个问题,都可以用中序线索二叉树有效地解决

  1. 在中序线索二叉树中找前驱和后继
// 找中序前驱ThreadBTreeNode*FindInOrderPreNode(ThreadBTreeNode*q){if(q!=NULL){if(q->ltag==1)returnq->lchild;else// q->ltag == 0{ThreadBTreeNode*p=q->lchild;while(p->rtag==0)// 找最右下的结点p=p->rchild;returnp;}}}// 找中序后继ThreadBTreeNode*FindInOrderPostNode(ThreadBTreeNode*q){if(q!=NULL){if(q->rtag==1)returnq->lchild;else// q->rtag == 0{ThreadBTreeNode*p=q->rchild;while(p->ltag==0)// 找最左下的结点p=p->lchild;returnp;}}}
  1. 从G节点开始继续往后遍历(本质就是不断地找G的后继结点)
// 找中序后继ThreadBTreeNode*FindInOrderPostNode(ThreadBTreeNode*q);// 打印结点数据voidvisit(ThreadBTreeNode*p);// 中序遍历中序线索二叉树voidInOrder(ThreadBTree T){if(T!=NULL){ThreadBTreeNode*first_node=T;if(T->ltag==0)first_node=T->lchild;while(first_node->ltag==0)first_node=first_node->lchild;// 线索二叉树T第一个被访问的结点for(ThreadBTreeNode*p=first_node;p!=NULL;p=FindInOrderPostNode(p)){visit(p);}}}

至此这两个问题就都解决啦😄

拓展部分

  1. 拓展先序线索二叉树找前驱和后继

// 找先序后继ThreadBTreeNode*FindPreOrderPostNode(ThreadBTreeNode p){if(p!=NULL){if(p->rtag==1)returnp->rchild;else{if(p->ltag==0)returnp->lchild;elsereturnp->rchild;}}}

假设可以从p结点访问到他的双亲,那么会出现以下情况

  1. 先序线索二叉树的先序遍历
// 找先序后继ThreadBTreeNode*FindPreOrderPostNode(ThreadBTreeNode p);// 打印p结点voidvisit(ThreadBTreeNode*p);// 先序遍历voidpreOrder(ThreadBTreeNode T){if(T!=NULL){ThreadBTreeNode*first_node=T;for(ThreadBTreeNode*p=first_node;p!=NULL;FindPreOrderPostNode(p)){visit(p);}}// 第一个被遍历的结点一定是根结点}


  1. 后序线索二叉树的前驱
// 找后序前驱ThreadBTreeNode*FindPostOrderPreNode(ThreadBTreeNode p){if(p!=NULL){if(p->ltag==1)returnp->lchild;else{if(p->rtag==0)returnp->rchild;elsereturnp->lchild;}}}

假设可以从p结点访问到他的双亲,那么会出现以下情况


中序线索二叉树找前驱和后继

总结:

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/3 21:49:29

Ubuntu 22.04企业级应用实战:搭建高可用Web服务

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 开发一个在Ubuntu 22.04上部署高可用Web服务的自动化脚本。要求包含:1) Nginx安装与基础配置 2) Lets Encrypt SSL证书自动申请 3) 负载均衡配置(可选用HAProxy) 4) 系…

作者头像 李华
网站建设 2026/2/5 5:33:39

用IDEA快速原型设计:1小时开发RESTful API

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 生成一个Spring Boot快速原型项目,要求:1. 使用IDEA的Spring Initializr创建项目 2. 集成MyBatis-Plus实现自动CRUD 3. 生成Swagger3接口文档 4. 包含Postm…

作者头像 李华
网站建设 2026/2/4 2:19:36

Vue-PDF开发效率对比:传统vs快马AI生成

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 请生成一份详细的对比报告,比较传统手动开发与AI辅助开发Vue-PDF组件的效率差异。要求包含:1. 开发时间对比数据;2. 代码质量分析;3…

作者头像 李华
网站建设 2026/2/5 2:24:09

5分钟原型:验证你的DECIMAL处理方案

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 在快马平台上快速开发一个原型应用,比较处理非终止小数的三种策略:1) 直接浮点运算;2) 设置最大小数位数;3) 分数形式保持。要求&am…

作者头像 李华
网站建设 2026/2/3 6:27:19

Flutter开发效率对比:传统vs快马AI辅助

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 请生成一个Flutter社交媒体应用的登录注册模块代码,包含:1.邮箱/手机号登录 2.第三方登录(微信、Google) 3.注册流程 4.密码找回功能。要求使用Firebase作为…

作者头像 李华