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


🎁个人主页: 工藤新一¹

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

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

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


文章目录

线索二叉树

🎯 📊 🔧 📝🎨 🛠️⚡ 📈🎯💡

一、基本概念

🎯 核心思想:变废为宝

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


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

📊 普通二叉树的问题

在普通二叉树中:

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.迭代更新 PreNodePreNode = 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中序遍历线索化二叉树思路

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


相比于前序线索二叉树中序线索二叉树 里并不只有一个节点指向 nullptrfirsrt-->left == nullptrultimate-->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)
查找前驱/后继 困难 容易
插入/删除复杂度 简单 复杂
预处理 无需 需要一次线索化
灵活性 高(支持多种遍历) 低(特定遍历优化)

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

推荐选择:

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

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

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

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

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


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

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