

🎁个人主页: 工藤新一¹
🔍系列专栏: 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)空间复杂度的遍历
- ✅ 操作便利:快速查找前驱和后继
**代价:**插入和删除操作更复杂,需要维护线索关系
🌟 各位看官好,我是工藤新一¹呀~
🌈 愿各位心中所想,终有所致!