🔗 "废物利用"的艺术:线索二叉树全解析!🌲
哈喽,各位代码世界的探险家们!👋
上次我们聊了二叉树遍历的“爱恨情仇”,今天我们来搞点更酷的——给二叉树“穿针引线”,也就是大名鼎鼎的线索二叉树 (Threaded Binary Tree)!
你有没有觉得,普通二叉树里那些指向 NULL 的指针好浪费?🤔 对于一个有 n 个节点的二叉树,足足有 n+1 个 NULL 指针!它们就像城市里闲置的土地,啥也没干,太可惜了!
核心思想: 与其让这些指针闲着,不如让它们“再就业”!用它们来指向节点在特定遍历次序下的前驱和后继。
这样一来,我们就把“废物”变成了“宝藏”💎,不仅节约了空间,还获得了一个超能力:无需递归或栈,就能光速找到节点的前驱和后继,并高效地遍历整棵树!
听起来是不是很棒?下面,就让我们一起揭开它的神秘面纱吧!
🧵 准备工作:节点的新装
为了区分指针是指向孩子(实线)还是线索(虚线),我们需要给节点结构加两个标志位:
LTag(Left Tag):0: 左指针指向左孩子 (Link)。1: 左指针指向前驱线索 (Thread)。
RTag(Right Tag):0: 右指针指向右孩子 (Link)。1: 右指针指向后继线索 (Thread)。
C++风格的伪代码结构看起来是这样的:
// C++ Style Pseudocode
struct ThreadNode {
DataType data;
ThreadNode* leftChild;
ThreadNode* rightChild;
int lTag; // 0 for link, 1 for thread
int rTag; // 0 for link, 1 for thread
};
接下来,我们将分类型探讨最经典的中序线索化,以及相对复杂的前序和后序线索化。
🥇 中序线索二叉树:最经典、最和谐的“穿针引线”
中序遍历(左 → 根 → 右)的顺序与节点的前驱、后继关系最为自然,因此中序线索化是最常用、最重要的一种!
- 线索化规则:
- 若节点的左指针为空,则将其指向它的中序前驱。
- 若节点的右指针为空,则将其指向它的中序后继。
🔍 如何寻找中序前驱 & 后继?
这正是中序线索树的魅力所在!
寻找后继 (Successor)
- 有线索吗? 如果节点的
rTag是1,那么rightChild指针直接就指向了它的后继!一步到位!🚀 - 没线索怎么办? 如果
rTag是0,说明它有右子树。根据中序遍历(左→根→右)的原则,它的后继就是其右子树中“最左边”的那个节点。
// C++ Style Pseudocode
ThreadNode* findInOrderSuccessor(ThreadNode* node) {
if (node->rTag == 1) {
return node->rightChild; // Case 1: Thread exists!
} else {
// Case 2: Go to the right subtree and find its leftmost node.
ThreadNode* p = node->rightChild;
while (p->lTag == 0) {
p = p->leftChild;
}
return p;
}
}
寻找前驱 (Predecessor)
逻辑完全对称!
- 有线索吗? 如果节点的
lTag是1,那么leftChild指针就指向了它的前驱。 - 没线索怎么办? 如果
lTag是0,说明它有左子树。它的前驱就是其左子树中“最右边”的那个节点。
// C++ Style Pseudocode
ThreadNode* findInOrderPredecessor(ThreadNode* node) {
if (node->lTag == 1) {
return node->leftChild; // Case 1: Thread exists!
} else {
// Case 2: Go to the left subtree and find its rightmost node.
ThreadNode* p = node->leftChild;
while (p->rTag == 0) {
p = p->rightChild;
}
return p;
}
}
🚶♂️ 如何遍历中序线索二叉树?
超级简单!我们再也不需要栈了!
- 从整棵树的“最左边”的节点开始。
- 循环执行:
- 访问当前节点。
- 通过我们刚才写的
findInOrderSuccessor函数找到下一个节点。
- 直到遍历完所有节点。
// C++ Style Pseudocode
void inOrderTraversal(ThreadNode* root) {
// 1. Find the very first node to visit (the leftmost one).
ThreadNode* p = root;
while (p->lTag == 0) {
p = p->leftChild;
}
// 2. Loop through all nodes using successor pointers.
while (p != nullptr) {
visit(p); // Process the current node
p = findInOrderSuccessor(p);
}
}
🥈 前序线索二叉树:老大的“前后关系”
前序遍历(根 → 左 → 右)的线索化稍微有些不同。
- 线索化规则:
- 若节点的左指针为空,则将其指向它的前序前驱。
- 若节点的右指针为空,则将其指向它的前序后继。
🔍 寻找前序前驱 & 后继?
寻找后继 (Successor)
这个比较简单!根据(根 → 左 → 右)的顺序:
- 如果它有左孩子,那么左孩子就是它的后继。
- 如果它没有左孩子,但有右孩子,那么右孩子就是后继。
- 如果它是个叶子节点,那么它的后继已经由
rightChild这条线索指明了。
// C++ Style Pseudocode
ThreadNode* findPreOrderSuccessor(ThreadNode* node) {
if (node->lTag == 0) {
return node->leftChild; // Has left child, it's the successor.
} else {
// No left child, the right pointer (either a link or a thread) points to the successor.
return node->rightChild;
}
}
寻找前驱 (Predecessor)
这个就复杂了!🤯 前序前驱可能是它的父节点,也可能是父节点的左兄弟子树中的某个节点。在没有父指针的情况下,仅通过当前节点和线索很难直接找到前驱。 这也是前序线索化不那么流行的原因之一。通常需要遍历或者借助父指针才能高效完成。
🚶♂️ 如何遍历前序线索二叉树?
遍历依然很高效!
- 从根节点开始。
- 循环执行:
- 访问当前节点。
- 通过
findPreOrderSuccessor找到下一个。
// C++ Style Pseudocode
void preOrderTraversal(ThreadNode* root) {
ThreadNode* p = root;
while (p != nullptr) {
visit(p);
p = findPreOrderSuccessor(p);
}
}
🥉 后序线索二叉树:压轴的“究极挑战”
后序遍历(左 → 右 → 根)的线索化是三者中最复杂的。
- 线索化规则:
- 若节点的左指针为空,则将其指向它的后序前驱。
- 若节点的右指针为空,则将其指向它的后序后继。
🔍 寻找后序前驱 & 后继?
寻找后继 (Successor)
后序后继可能是它的父节点,或者是它父节点的右兄弟。和前序前驱一样,在没有父指针的情况下,后序找后继也非常困难!
寻找前驱 (Predecessor)
这个反而相对清晰!根据(左 → 右 → 根)的顺序:
- 如果它有右孩子,那么右孩子就是它的前驱。
- 如果它没有右孩子,但有左孩子,那么左孩子就是前驱。
- 如果它是个叶子节点,它的前驱已经由
leftChild这条线索指明了。
// C++ Style Pseudocode
ThreadNode* findPostOrderPredecessor(ThreadNode* node) {
if (node->rTag == 0) {
return node->rightChild; // Has right child, it's the predecessor.
} else if (node->lTag == 0) {
return node->leftChild; // No right child but has left child, it's the predecessor.
} else {
return node->leftChild; // Is a leaf, leftChild is a thread to the predecessor.
}
}
🚶♂️ 如何遍历后序线索二叉树?
由于找后继很困难,后序遍历通常是逆向的!即从最后一个节点开始,不断寻找前驱。
- 找到整棵树的根节点(它是最后一个被访问的)。
- 找到它的前驱,然后是前驱的前驱……
- 这样得到的序列反过来就是后序遍历序列了。
因为这种复杂性,后序线索二叉树在实际应用中非常罕见。
🏁 横向大比拼 & 总结
| 特性 | 中序线索树 (In-order) | 前序线索树 (Pre-order) | 后序线索树 (Post-order) |
|---|---|---|---|
| 实现难度 | ⭐⭐ (简单) | ⭐⭐⭐ (中等) | ⭐⭐⭐⭐⭐ (困难) |
| 找后继 | ✅ 非常容易 | ✅ 比较容易 | ❌ 非常困难 (需父指针) |
| 找前驱 | ✅ 非常容易 | ❌ 比较困难 (需父指针) | ✅ 比较容易 |
| 遍历方式 | 正向遍历,方便 | 正向遍历,方便 | 逆向遍历,不直观 |
| 应用广泛度 | 最常用 👍 | 较少使用 | 几乎不用 👎 |
总而言之,线索二叉树的核心价值在于:
- 空间高效: 盘活了所有
NULL指针,一滴都不浪费! - 时间高效: 实现了非递归、无栈的快速遍历,以及对前驱/后继的快速定位(尤其在中序中)。
它就像是对普通二叉树的一次精巧的“魔改”,虽然增加了一点点实现的复杂性,却换来了巨大的便利。特别是中序线索二叉树,绝对是值得你掌握的经典数据结构!
希望这篇博客能让你对线索二叉树有一个清晰又有趣的认识!快去动手实现一个吧,你会爱上这种“穿针引线”的感觉的!
觉得有用别忘了点赞分享哦!下次见啦!💖

被折叠的 条评论
为什么被折叠?



