

🎁个人主页: 工藤新一¹
 🔍系列专栏: C++面向对象(类和对象篇)
 🌟心中的天空之城,终会照亮我前方的路
 🎉欢迎大家点赞👍评论📝收藏⭐文章
文章目录
- 线索二叉树
 - 
- 一、基本概念
 - 
- 
- [🎯 核心思想:变废为宝](#🎯 核心思想:变废为宝)
 
 
 - 
 - 二、为什么需要线索二叉树?
 - 
- 
- [📊 普通二叉树的问题](#📊 普通二叉树的问题)
 
 
 - 
 - 三、线索化规则
 - 
- 
- [📝 节点结构定义](#📝 节点结构定义)
 
 
 - 
 - 四、线索化二叉树
 - 
- 4.1前序遍历
 - 
- 
- [⚡ 核心优势:高效遍历](#⚡ 核心优势:高效遍历)
 
 - 4.1.1前序遍历线索化二叉树思路
 - 
- [🎨 直观理解](#🎨 直观理解)
 
 - 4.1.2🔄前序遍历线索化二叉树实现
 
 - 
 - 4.2中序遍历
 - 4.3后序遍历
 
 - 五、小结
 - 
- 
- 
- [📈 ✅性能对比](#📈 ✅性能对比)
 - [🎯 适用场景](#🎯 适用场景)
 
 
 - 
 
 - 
 
 
线索二叉树
🎯 📊 🔧 📝🎨 🛠️⚡ 📈🎯💡
一、基本概念
🎯 核心思想:变废为宝
普通二叉树 中有大量空指针域 ("线索二叉树是对普通二叉树的改进 "),线索二叉树 利用这些空指针域来存储遍历顺序的前驱和后继信息 ,从而实现对二叉树 的高效遍历
二、为什么需要线索二叉树?
📊 普通二叉树的问题
在普通二叉树中:
            
            
              cpp
              
              
            
          
          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 的节点才需线索化
            
            
              cpp
              
              
            
          
          C++
    // 线索标志
	typedef enum 
	{
  		CHILD, // 0
   		THREAD // 1 节点指向nullptr
	}PointerTag; // 枚举:孩子 or 线索
	
	// 定义树节点的存储类型
	typedef int ElemType;
	typedef struct ThreadTreeNode
	{
    	ElemType data;
    	struct ThreadTreeNode* left, * right;
    	// 线索标志默认为 0
    	PointerTag 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.迭代更新
PreNode,PreNode = A;(设置当前节点为前一节点) 
 - 
 - 
3.单独处理最后一个节点的右指针线索[默认指向
nullptr,修改rtag = 0] 

黄色:前驱指针
蓝色:后继指针

技巧:
- 在当前节点A,设置A前驱:X<--A
 - 在当前节点的后一节点B,设置A后继:A-->B, A<--B
 
🎨 直观理解
原始二叉树:
            
            
              asciiarmor
              
              
            
          
                  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):
            
            
              asciiarmor
              
              
            
          
                  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:定义全局变量,记录遍历过程中的前驱节点
            
            
              cpp
              
              
            
          
          C++
	ThreadTreeNode* PreNode = nullptr;
        步骤2:前序遍历实现二叉树线索化(前序线索化递归函数)
            
            
              cpp
              
              
            
          
          // 前序遍历实现二叉树线索化
void PreOrderThread(ThreadTreeNode* node)
{
    if (node == nullptr) return;
    
    // 无需访问节点数据
//    cout << node->data << "->"; 遍历目的:为空链域设置线索,将二叉树转变为线索二叉树
    
// 2.递归式访问空链域
    // a.为当前节点设置前驱节点(如何为当前节点设置前驱节点?当前节点什么情况下需要设置前驱节点?)
    
    // 检查当前节点左子树是否为空
    if (!node->left)
    {
        // 为空:建立当前节点的左指针线索,指向 PreNode
        node->left = PreNode;
        // 更新标志位
        node->ltag = THREAD;
    }
    
    // b.为当前节点的前一节点设置后继线索(何时需要设置右指针线索?)
    // 检查当前节点右子树是否为空 && PreNode != nullptr(防止空指针异常)
    if (PreNode && !PreNode->right)
    {
        PreNode->right = node;
        PreNode->rtag = THREAD;
    }
    // c.迭代更新 PreNode
    PreNode = node;
cout << "----------- 此时就完成了对空链域的线索化 -----------" << endl;
    
    // d.添加条件限制:当 ltag == 0时向下递归,ltag == 1不需递归(会导致无限递归)
    // 因为标志位可能已经被设置为THREAD,但我们需要递归真实的子树
    if(node->ltag == CHILD)
        PreOrderThread(node->left);
   
    if(node->rtag == CHILD)
        PreOrderThread(node->right);
}
        

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


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

            
            
              cpp
              
              
            
          
          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):
            
            
              cpp
              
              
            
          
          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中序遍历线索化二叉树思路
核心思路:左-->根-->右

相比于前序线索二叉树 ,中序线索二叉树 里并不只有一个节点指向 nullptr:firsrt-->left == nullptr ;ultimate-->right = nullptr;

4.2.2🔄中序遍历线索化二叉树实现
            
            
              cpp
              
              
            
          
          ThreadTreeNode* PreNode = nullptr;
void InOrderThread(ThreadTreeNode* node)
{
    if (node == nullptr) return;
    // 递归访问左子树
    InOrderThread(node->left);
    // a.设置前驱(此时 node == ultimate.node)
    if (!node->left)
    {
        node->left = PreNode; // 默认值nullptr
        node->ltag = THREAD;
    }
    
    // b.为当前节点的前驱节点设置后继线索
    if (PreNode && !PreNode->right)
    {
        // 此时,PreNode 仍为 node的前继节点
        PreNode->right = node;
        PreNode->rtag = THREAD;
    }
    // c.迭代更新 PreNode
    PreNode = 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->left
        firstNode = firstNode->left;
    
    // 2.依次访问线索二叉树的节点
    ThreadTreeNode* cursor = firstNode;
    // 易错:最后节点 nullptr
    while (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) | 
| 查找前驱/后继 | 困难 | 容易 | 
| 插入/删除复杂度 | 简单 | 复杂 | 
| 预处理 | 无需 | 需要一次线索化 | 
| 灵活性 | 高(支持多种遍历) | 低(特定遍历优化) | 
🎯 适用场景
- 频繁遍历但很少修改的数据
 - 内存受限的环境
 - 需要快速查找前驱/后继的操作
 - 数据库索引结构
 - 编译器语法树
 
推荐选择:
- 普通二叉树:简单应用、多种遍历需求、内存充足
 - 前序线索二叉树:频繁前序遍历、内存受限、实时性要求高
 
线索二叉树 是"用编程复杂度换取运行效率"的典型例子
**线索二叉树的本质:**通过利用空指针域存储遍历顺序信息,实现:
- ✅ 空间效率:100%指针利用率、
 - ✅ 时间效率:O(1)空间复杂度的遍历
 - ✅ 操作便利:快速查找前驱和后继
 
**代价:**插入和删除操作更复杂,需要维护线索关系
🌟 各位看官好,我是工藤新一¹呀~
🌈 愿各位心中所想,终有所致!