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.迭代更新
PreNode
,PreNode = 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中序遍历线索化二叉树思路
核心思路:左–>根–>右
相比于前序线索二叉树,中序线索二叉树里并不只有一个节点指向 nullptr
:firsrt–>left == nullptr
;ultimate–>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) |
查找前驱/后继 | 困难 | 容易 |
插入/删除复杂度 | 简单 | 复杂 |
预处理 | 无需 | 需要一次线索化 |
灵活性 | 高(支持多种遍历) | 低(特定遍历优化) |
🎯 适用场景
- 频繁遍历但很少修改的数据
- 内存受限的环境
- 需要快速查找前驱/后继的操作
- 数据库索引结构
- 编译器语法树
推荐选择:
- 普通二叉树:简单应用、多种遍历需求、内存充足
- 前序线索二叉树:频繁前序遍历、内存受限、实时性要求高
线索二叉树 是"用编程复杂度换取运行效率"的典型例子
**线索二叉树的本质:**通过利用空指针域存储遍历顺序信息,实现:
- ✅ 空间效率:100%指针利用率、
- ✅ 时间效率:O(1)空间复杂度的遍历
- ✅ 操作便利:快速查找前驱和后继
**代价:**插入和删除操作更复杂,需要维护线索关系
🌟 各位看官好,我是工藤新一¹呀~
🌈 愿各位心中所想,终有所致!