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/6 5:22:27

RDPWRAP新手指南:5分钟实现Windows多用户远程桌面

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 创建一个交互式新手教程应用,逐步引导用户完成RDPWRAP的安装和配置。应用应包含:1) 图文并茂的步骤说明 2) 实时系统检测功能 3) 一键式问题修复 4) 视频演…

作者头像 李华
网站建设 2026/2/4 6:26:48

传统CV vs HALCON:图像处理效率对比实验报告

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 设计一个对比实验项目,分别使用HALCON和OpenCV实现相同的图像处理流程:1) 模板匹配 2) 边缘检测 3) 几何测量。要求:a) 使用相同测试图像集 b) …

作者头像 李华
网站建设 2026/1/30 5:13:29

【Android 性能分析】延伸阅读:新版的Profiler

Android Studio Profiler Task 在Android开发中,“性能优化”是绕不开的课题——卡顿、内存泄漏、耗电快等问题,往往藏在代码细节里,靠“猜”很难定位。 新版Android Studio Profiler的任务工具,正是帮开发者从“盲调”转向“精准…

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

零基础入门:5分钟学会随机森林算法

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 创建一个面向初学者的随机森林教学项目。要求:1) 用最简单语言解释算法原理;2) 提供step-by-step代码示例;3) 包含可交互的演示界面&#xff1b…

作者头像 李华
网站建设 2026/1/28 4:07:45

MONACO-EDITOR实战:构建在线IDE的完整指南

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 开发一个完整的在线IDE,使用MONACO-EDITOR作为核心编辑器。要求支持多文件项目管理,提供终端模拟器,集成Git版本控制功能,并允许用户…

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

AI如何帮你快速掌握React Server Components开发

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容: 请生成一个React Server Components的示例项目,包含以下功能:1) 展示服务器端数据获取的组件 2) 客户端交互组件的实现 3) 两者之间的通信机制。使用Next.j…

作者头像 李华