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

C/C++ 数据结构 —— 线索二叉树

在这里插入图片描述

在这里插入图片描述


🎁个人主页:工藤新一¹

🔍系列专栏:C++面向对象(类和对象篇)

​ 🌟心中的天空之城,终会照亮我前方的路

🎉欢迎大家点赞👍评论📝收藏⭐文章


文章目录

  • 线索二叉树
    • 一、基本概念
        • 🎯 核心思想:变废为宝
    • 二、为什么需要线索二叉树?
        • 📊 普通二叉树的问题
    • 三、线索化规则
        • 📝 节点结构定义
    • 四、线索化二叉树
      • 4.1前序遍历
          • ⚡ 核心优势:高效遍历
        • 4.1.1前序遍历线索化二叉树思路
          • 🎨 直观理解
        • 4.1.2🔄前序遍历线索化二叉树实现
      • 4.2中序遍历
        • 4.2.1中序遍历线索化二叉树思路
        • 4.2.2🔄中序遍历线索化二叉树实现
      • 4.3后序遍历
    • 五、小结
          • 📈 ✅性能对比
          • 🎯 适用场景

线索二叉树

🎯 📊 🔧 📝🎨 🛠️⚡ 📈🎯💡

一、基本概念

🎯 核心思想:变废为宝

普通二叉树中有大量空指针域(“线索二叉树是对普通二叉树的改进”),线索二叉树 利用这些空指针域来存储遍历顺序的前驱和后继信息,从而实现对二叉树高效遍历


二、为什么需要线索二叉树?

📊 普通二叉树的问题

在普通二叉树中:

C++typedef struct TreeNode{int val;struct TreeNode* left;struct TreeNode* right;}TreeNode;

问题:

  • 空指针浪费:约有 50% 的指针域为空(n个节点有2n个指针,实际只用n-1个)

  • 遍历效率低:需要**递归或栈[循环遍历]**,空间复杂度高


三、线索化规则

解决方案:根据遍历策略进行相应的线索化

为每个节点增加两个标志位

  • ltag:0表示left指向左孩子,1表示left指向前驱
  • rtag:0表示right指向右孩子,1表示right指向后继

在这里插入图片描述


📝 节点结构定义

注意:度为 0/1 的节点才需线索化

C++// 线索标志typedef enum {CHILD, // 0THREAD // 1 节点指向nullptr}PointerTag; // 枚举:孩子 or 线索// 定义树节点的存储类型typedef int ElemType;typedef struct ThreadTreeNode{ElemType data;struct ThreadTreeNode* left, * right;// 线索标志默认为 0PointerTag ltag;PointerTag rtag;// 初始化字段 - 也要添加标志位初始化ThreadTreeNode(int val) : data(val), left(nullptr), right(nullptr), ltag(CHILD), rtag(CHILD){ }} ThreadTreeNode;

在这里插入图片描述


四、线索化二叉树

线索二叉树(链式存储)

一个二叉树想成为 线索二叉树,必须要基于遍历方式的基础上从而 线索化

注意:度为 0/1 的节点才需线索化


4.1前序遍历

问:如何构造前序线索二叉树呢?
答:在对二叉树进行前序遍历的过程中,为二叉树的空链域线索化

⚡ 核心优势:高效遍历

普通二叉树前序遍历(需要栈):

递归实现 OR 循环实现(显式使用栈)


线索二叉树前序遍历(无需栈):

递归实现 OR 循环实现


4.1.1前序遍历线索化二叉树思路

第一步: 前序遍历为每个节点构造线索(使用前序遍历,实现二叉树线索化

  • 1.定义全局变量:记录遍历过程中的前驱节点,PreNode = nullptr;

  • 2.使用前序遍历,递归式地为每一节点的空链域设置线索:

    • a.寻找空链域

    • b.为当前节点设置前驱节点

      b1.检查当前节点的左孩子节点是否为空
      若为空:建立当前节点的左指针线索,指向 PreNode(当前节点的前驱节点)
      若为不空:说明该节点是从根节点到当前路径上的任意一个节点,继续递归式访问左子树

    • c.为当前节点[A]设置后继线索[B](准确的可以理解为:在后继节点[B]设置前驱节点[A]),即给后一节点设置前驱线索[不对,因为目前还未获取后一节点的信息!];或:为当前节点[A]的前继节点[C]设置后继线索[C->A]
      c1.检查前一节点的右子树是否为空
      若为空:建立前一节点的右指针线索[C],指向当前节点[C->A]
      若不为空:不做处理

    • d.迭代更新 PreNodePreNode = A;(设置当前节点为前一节点)

  • 3.单独处理最后一个节点的右指针线索[默认指向 nullptr,修改rtag = 0]

在这里插入图片描述

黄色:前驱指针

蓝色:后继指针


在这里插入图片描述

技巧:

  • 在当前节点A,设置A前驱:X<–A
  • 在当前节点的后一节点B,设置A后继:A–>B, A<–B

🎨 直观理解

原始二叉树:

        A(1)/   \B(2)  C(3)/   \D(4)  E(5)

空指针:D.left, D.right, E.left, E.right, C.left, C.right
前序遍历顺序:A(1) → B(2) → D(4) → E(5) → C(3)


前序线索化后(中序遍历:D→B→E→A→C→F):

        A(1)/   \B(2) → C(3)/   \ D(4) → E(5) → C(3)
  • D.right → E (后继)
  • E.right → C (后继)
  • C.right → nullptr (最后一个节点)

4.1.2🔄前序遍历线索化二叉树实现

步骤1:定义全局变量,记录遍历过程中的前驱节点

C++ThreadTreeNode* PreNode = nullptr;

步骤2:前序遍历实现二叉树线索化(前序线索化递归函数

// 前序遍历实现二叉树线索化
void PreOrderThread(ThreadTreeNode* node)
{if (node == nullptr) return;// 无需访问节点数据
//    cout << node->data << "->"; 遍历目的:为空链域设置线索,将二叉树转变为线索二叉树// 2.递归式访问空链域// a.为当前节点设置前驱节点(如何为当前节点设置前驱节点?当前节点什么情况下需要设置前驱节点?)// 检查当前节点左子树是否为空if (!node->left){// 为空:建立当前节点的左指针线索,指向 PreNodenode->left = PreNode;// 更新标志位node->ltag = THREAD;}// b.为当前节点的前一节点设置后继线索(何时需要设置右指针线索?)// 检查当前节点右子树是否为空 && PreNode != nullptr(防止空指针异常)if (PreNode && !PreNode->right){PreNode->right = node;PreNode->rtag = THREAD;}// c.迭代更新 PreNodePreNode = node;cout << "----------- 此时就完成了对空链域的线索化 -----------" << endl;// d.添加条件限制:当 ltag == 0时向下递归,ltag == 1不需递归(会导致无限递归)// 因为标志位可能已经被设置为THREAD,但我们需要递归真实的子树if(node->ltag == CHILD)PreOrderThread(node->left);if(node->rtag == CHILD)PreOrderThread(node->right);
}

在这里插入图片描述


在这里插入图片描述


步骤3:二叉树线索化入口 - 实现二叉树线索化过程 与 最后节点处理过程分离(前序线索二叉树入口函数

在这里插入图片描述


在这里插入图片描述


void CreatePreOrderThread(ThreadTreeNode* root)
{
/*细节补充:为全局变量重新初始化 - 非必须,习惯!因为全局变量的使用可能会被其他函数进行操作,可能会导致其变为非正常值
*/PreNode = nullptr;if (root){ // 2'.使用前序遍历,递归式地为每个节点的空链域设置线索PreOrderThread(root);// 3.单独处理最后一个节点的右指针线索(二叉树线索化完成之后)if (PreNode){PreNode->rtag = THREAD;// PreNode->right == nullptr(默认);因此无需设置}
}

第二步: 2.定义函数,遍历 线索二叉树

🔍递归(Recursion):

在这里插入图片描述


void PreOrderByRec(ThreadTreeNode* node)
{if (node == nullptr) return;cout << node->data << "->";// 根据线索标志决策指向对应递归路线if(node->ltag == CHILD) // node->left 指向子树PreOrderByRec(node->left);else if(node->ltag == THREAD) // node->left 指向前驱节点PreOrderByRec(node->right);
}

🔍循环(Circulate):

void PreOrderByFor(ThreadTreeNode* node)
{ThreadTreeNode* cursor = node;while (cursor != nullptr){cout << cursor->data << "->";if (cursor->ltag == CHILD) // 向左子树循环cursor = cursor->left;else if (cursor->ltag == THREAD) // 向后继节点循环cursor = cursor->right;}
}

4.2中序遍历

问:如何构造中序线索二叉树呢?
答:在对二叉树中序遍历的过程中,为二叉树的空链域进行线索化

重点:一定要分清前序线索化和中序线索化的区别!!!

在这里插入图片描述

在这里插入图片描述


4.2.1中序遍历线索化二叉树思路

核心思路:左–>根–>右

在这里插入图片描述


相比于前序线索二叉树中序线索二叉树里并不只有一个节点指向 nullptrfirsrt–>left == nullptrultimate–>right = nullptr;

在这里插入图片描述


4.2.2🔄中序遍历线索化二叉树实现
ThreadTreeNode* PreNode = nullptr;void InOrderThread(ThreadTreeNode* node)
{if (node == nullptr) return;// 递归访问左子树InOrderThread(node->left);// a.设置前驱(此时 node == ultimate.node)if (!node->left){node->left = PreNode; // 默认值nullptrnode->ltag = THREAD;}// b.为当前节点的前驱节点设置后继线索if (PreNode && !PreNode->right){// 此时,PreNode 仍为 node的前继节点PreNode->right = node;PreNode->rtag = THREAD;}// c.迭代更新 PreNodePreNode = node;// 直接递归式访问后继节点,不会存在无限递归,所以无需添加条件限制InOrderThread(node->right); // node5->right == node7!因为中序遍历规则!578910...
}void CreateInOrderThread(ThreadTreeNode* root)
{PreNode = nullptr;if (root != nullptr){InOrderThread(root);if (PreNode)PreNode->rtag = THREAD;}
}// 前序遍历的首节点是根节点 - 中序遍历的首节点是左子树节点
void InOrderByFor(ThreadTreeNode* node)
{if (!node) return;// 1.获取第一个节点ThreadTreeNode* firstNode = node;while (firstNode->ltag == 0) // 可以使用 node->leftfirstNode = firstNode->left;// 2.依次访问线索二叉树的节点ThreadTreeNode* cursor = firstNode;// 易错:最后节点 nullptrwhile (cursor){// 访问节点数据cout << cursor->data << "->";/*如何找到当前节点的后继节点?- right指针指向的值:父节点 or 右子树需要区分当前节点的后继节点,即 node->right == 父节点?右子树?由于 rtag 的值不同,node->right 指向含义不同,因此需区分对待*/ if (cursor->rtag == CHILD){cursor = cursor->right;// 不能使用 cursor->left判断// 因为中序遍历的 node->left == nullptr;只出现在最左子树中while (cursor->ltag == 0)cursor = cursor->left;}else if (cursor->rtag == THREAD) // {cursor = cursor->right;}}
}

4.3后序遍历

问:如何构造后序线索二叉树呢?
答:在对二叉树后续遍历过程中,为二叉树空链域线索化

在这里插入图片描述


五、小结

📈 ✅性能对比
特性普通二叉树线索二叉树
空间利用率50%指针空闲100%指针利用
遍历空间复杂度O(h)O(1)
查找前驱/后继困难容易
插入/删除复杂度简单复杂
预处理无需需要一次线索化
灵活性高(支持多种遍历)低(特定遍历优化)

🎯 适用场景
  1. 频繁遍历但很少修改的数据
  2. 内存受限的环境
  3. 需要快速查找前驱/后继的操作
  4. 数据库索引结构
  5. 编译器语法树

推荐选择:

  • 普通二叉树:简单应用、多种遍历需求、内存充足
  • 前序线索二叉树:频繁前序遍历、内存受限、实时性要求高

线索二叉树 是"用编程复杂度换取运行效率"的典型例子

**线索二叉树的本质:**通过利用空指针域存储遍历顺序信息,实现:

  • 空间效率:100%指针利用率、
  • 时间效率:O(1)空间复杂度的遍历
  • 操作便利:快速查找前驱和后继

**代价:**插入和删除操作更复杂,需要维护线索关系


🌟 各位看官好,我是工藤新一¹呀~

🌈 愿各位心中所想,终有所致!

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

相关文章:

  • 《联盟》书籍解读总结
  • 基于NXP iMXRT600音频算法开发方法
  • sql mvcc机制
  • PyTorch中的激活函数
  • html pc和移动端共用一个页面,移动端通过缩放达到适配页面,滚动飘窗
  • 实现自己的AI视频监控系统-第二章-AI分析模块3(核心)
  • ffmpeg+opencv交叉编译
  • Office 2024 长期支持版(Mac中文)Word、Execl、PPT
  • 使用Java操作微软 Azure Blob Storage:上传和下载文件
  • AR眼镜 + 视觉大模型在工业巡检的核心应用场景
  • AI赋能前端性能优化:核心技术与实战策略
  • “一带一路软件博览馆2025”香港开幕,金山办公田然:软件正重新定义未来生产力
  • 人机交互如何变革科普展示?哪些技术正成吸睛焦点?
  • 东芝2000AC彩机开机报请求维修CD40维修解决案例1
  • Kafka 4.0 升级实战路线图、关键变更与避坑清单
  • 2024年06月 Python(三级)真题解析#中国电子学会#全国青少年软件编程等级考试
  • 项目概要设计说明文档
  • 微服务-30.配置管理-动态路由
  • 当AI有了温度,三星正在重新定义生活的边界
  • Ubuntu安装Zookeeper全攻略
  • [Windows] WPS官宣 64位正式版(12.1.0.22525)全新发布!
  • leetcode274:H指数(计数排序)从 O(N²) 到 O(N) 的思维上升
  • 重学JS-004 --- JavaScript算法与数据结构(四)JavaScript 表单验证
  • Linux 下 Nginx 服务器从入门到精通:安装、配置、实战与性能优化​
  • mfc中操作excel
  • 关于国产 RAC 和分布式研讨
  • 【DBCExcelConvent】CAN报文解析辅助工具之DBC与Excel互转
  • 使用k8s实现部署MySQL的主从复制
  • 【LeetCode - 每日1题】求网格最长V形对角线段的长度
  • 页面跳转html