一、什么是线索二叉树
1.1 空指针的利用
对于 n 个节点的二叉树,有 2n 个指针域,其中 n-1 个指向实际节点,其余 n+1 个是空指针。
线索二叉树(Threaded Binary Tree):
-
如果左孩子为空,则指向前驱节点
-
如果右孩子为空,则指向后继节点
-
需要增加标志位区分是指向孩子还是线索
1.2 节点结构
c
typedef struct ThreadNode {
int data;
struct ThreadNode *left;
struct ThreadNode *right;
int ltag; // 0: 指向左孩子, 1: 指向前驱线索
int rtag; // 0: 指向右孩子, 1: 指向后继线索
} ThreadNode, *ThreadTree;
画个图理解:
text
普通节点: 线索化后:
[data] [data]
/ \ / \
left right left right
↓ ↓ ↓ ↓
孩子 孩子 前驱/ 后继/
孩子 孩子
二、中序线索化
2.1 算法思路
中序线索化就是在中序遍历的过程中,把空指针指向其前驱或后继。
关键 :用一个指针 pre 记录当前节点的前驱节点。
text
中序遍历顺序:左子树 → 根 → 右子树
线索化规则:
- 如果当前节点的左孩子为空,left 指向前驱(pre),ltag = 1
- 如果 pre 的右孩子为空,pre->right 指向当前节点,rtag = 1
- 更新 pre = 当前节点
2.2 手动推导
以这棵树为例:
text
1
/ \
2 3
/ \ \
4 5 6
中序遍历结果:4 2 5 1 3 6
线索化过程:
| 当前节点 | pre | 操作 |
|---|---|---|
| 4 | NULL | 4左空→左指NULL;pre为空,不处理右 |
| 2 | 4 | 2左不空;4右空→4右指向2 |
| 5 | 2 | 5左空→左指2;2右不空 |
| 1 | 5 | 1左不空;5右空→5右指向1 |
| 3 | 1 | 3左不空;1右不空 |
| 6 | 3 | 6左空→左指3;3右空→3右指向6 |
2.3 代码实现
c
#include <stdio.h>
#include <stdlib.h>
typedef struct ThreadNode {
int data;
struct ThreadNode *left;
struct ThreadNode *right;
int ltag; // 0: left指向左孩子, 1: left指向前驱
int rtag; // 0: right指向右孩子, 1: right指向后继
} ThreadNode, *ThreadTree;
ThreadNode* createNode(int value) {
ThreadNode *node = (ThreadNode*)malloc(sizeof(ThreadNode));
node->data = value;
node->left = NULL;
node->right = NULL;
node->ltag = 0;
node->rtag = 0;
return node;
}
// 构建测试树
ThreadTree buildTree() {
ThreadNode *root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
root->right->right = createNode(6);
return root;
}
ThreadNode *pre = NULL; // 全局变量,记录前驱节点
// 中序线索化(递归)
void inThread(ThreadTree root) {
if (root == NULL) return;
// 线索化左子树
inThread(root->left);
// 处理当前节点
if (root->left == NULL) {
root->left = pre;
root->ltag = 1;
}
if (pre != NULL && pre->right == NULL) {
pre->right = root;
pre->rtag = 1;
}
pre = root;
// 线索化右子树
inThread(root->right);
}
// 中序线索化入口
void createInThread(ThreadTree root) {
pre = NULL;
if (root != NULL) {
inThread(root);
// 处理最后一个节点的右指针
if (pre != NULL && pre->right == NULL) {
pre->rtag = 1;
}
}
}
三、中序线索二叉树的遍历
3.1 找后继节点
在中序线索树中,一个节点的后继有两种情况:
-
如果
rtag == 1,后继就是right(线索) -
如果
rtag == 0,后继是右子树中最左边的节点
c
// 找以p为根的子树中最左边的节点
ThreadNode* leftMost(ThreadNode *p) {
if (p == NULL) return NULL;
while (p->ltag == 0) { // 一直往左走
p = p->left;
}
return p;
}
// 找中序后继
ThreadNode* inSuccessor(ThreadNode *p) {
if (p->rtag == 1) {
return p->right; // 线索直接指向后继
} else {
return leftMost(p->right); // 右子树最左边
}
}
3.2 中序遍历
c
// 中序遍历线索二叉树(不需要栈和递归)
void inOrderTraversal(ThreadTree root) {
if (root == NULL) return;
// 找到最左边的节点(中序第一个)
ThreadNode *p = leftMost(root);
while (p != NULL) {
printf("%d ", p->data);
p = inSuccessor(p);
}
}
四、完整代码演示
c
#include <stdio.h>
#include <stdlib.h>
typedef struct ThreadNode {
int data;
struct ThreadNode *left;
struct ThreadNode *right;
int ltag;
int rtag;
} ThreadNode, *ThreadTree;
ThreadNode* createNode(int value) {
ThreadNode *node = (ThreadNode*)malloc(sizeof(ThreadNode));
node->data = value;
node->left = NULL;
node->right = NULL;
node->ltag = 0;
node->rtag = 0;
return node;
}
ThreadTree buildTree() {
ThreadNode *root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
root->right->right = createNode(6);
return root;
}
ThreadNode *pre = NULL;
void inThread(ThreadTree root) {
if (root == NULL) return;
inThread(root->left);
if (root->left == NULL) {
root->left = pre;
root->ltag = 1;
}
if (pre != NULL && pre->right == NULL) {
pre->right = root;
pre->rtag = 1;
}
pre = root;
inThread(root->right);
}
void createInThread(ThreadTree root) {
pre = NULL;
if (root != NULL) {
inThread(root);
if (pre != NULL && pre->right == NULL) {
pre->rtag = 1;
}
}
}
ThreadNode* leftMost(ThreadNode *p) {
if (p == NULL) return NULL;
while (p->ltag == 0) {
p = p->left;
}
return p;
}
ThreadNode* inSuccessor(ThreadNode *p) {
if (p->rtag == 1) {
return p->right;
} else {
return leftMost(p->right);
}
}
void inOrderTraversal(ThreadTree root) {
if (root == NULL) return;
ThreadNode *p = leftMost(root);
while (p != NULL) {
printf("%d ", p->data);
p = inSuccessor(p);
}
}
// 递归中序遍历(用于对比)
void recursiveInorder(ThreadTree root) {
if (root == NULL) return;
recursiveInorder(root->left);
printf("%d ", root->data);
recursiveInorder(root->right);
}
// 打印线索信息
void printThreadInfo(ThreadTree root) {
if (root == NULL) return;
printThreadInfo(root->left);
printf("节点%d: ltag=%d, rtag=%d", root->data, root->ltag, root->rtag);
if (root->ltag == 1 && root->left != NULL) {
printf(", 前驱=%d", root->left->data);
}
if (root->rtag == 1 && root->right != NULL) {
printf(", 后继=%d", root->right->data);
}
printf("\n");
printThreadInfo(root->right);
}
int main() {
ThreadTree root = buildTree();
printf("原二叉树(递归中序): ");
recursiveInorder(root);
printf("\n");
printf("\n--- 开始中序线索化 ---\n");
createInThread(root);
printf("\n线索信息:\n");
printThreadInfo(root);
printf("\n线索二叉树中序遍历: ");
inOrderTraversal(root);
printf("\n");
return 0;
}
运行结果:
text
原二叉树(递归中序): 4 2 5 1 3 6
--- 开始中序线索化 ---
线索信息:
节点4: ltag=1, rtag=1, 前驱=0, 后继=2
节点2: ltag=0, rtag=0
节点5: ltag=1, rtag=1, 前驱=2, 后继=1
节点1: ltag=0, rtag=0
节点3: ltag=0, rtag=1, 后继=6
节点6: ltag=1, rtag=1, 前驱=3, 后继=0
线索二叉树中序遍历: 4 2 5 1 3 6
五、前序和后序线索化
5.1 前序线索化
前序线索化与前序遍历类似,区别在于处理当前节点的时机在左右子树之前。
c
void preThread(ThreadTree root) {
if (root == NULL) return;
// 处理当前节点(在递归左右子树之前)
if (root->left == NULL) {
root->left = pre;
root->ltag = 1;
}
if (pre != NULL && pre->right == NULL) {
pre->right = root;
pre->rtag = 1;
}
pre = root;
// 注意:如果左子树是线索,不要递归进去
if (root->ltag == 0) {
preThread(root->left);
}
if (root->rtag == 0) {
preThread(root->right);
}
}
5.2 三种线索化对比
| 线索化类型 | 遍历顺序 | 后继查找 | 应用场景 |
|---|---|---|---|
| 中序线索化 | 左根右 | 最方便 | 最常用,支持双向遍历 |
| 前序线索化 | 根左右 | 较复杂 | 需要快速前序遍历 |
| 后序线索化 | 左右根 | 最复杂 | 较少使用 |
六、线索二叉树的优缺点
| 优点 | 缺点 |
|---|---|
| 遍历不需要栈或递归,节省空间 | 插入删除操作需要维护线索 |
| 可以快速找到前驱和后继 | 增加了标志位的空间开销 |
| 中序遍历时间复杂度O(n) | 线索化过程本身需要遍历 |
七、小结
这一篇我们学习了线索二叉树:
| 要点 | 说明 |
|---|---|
| 核心思想 | 利用空指针指向前驱/后继,避免空间浪费 |
| 节点结构 | 增加 ltag 和 rtag 区分孩子和线索 |
| 中序线索化 | 在中序遍历过程中设置线索 |
| 中序遍历 | 从最左节点开始,不断找后继 |
| 时间复杂度 | O(n),不需要递归栈 |
中序线索化的关键:
-
用
pre记录前驱节点 -
当前节点的左空 → 指向 pre
-
pre 的右空 → 指向当前节点
下一篇我们讲树、森林与二叉树的转换。
八、思考题
-
为什么中序线索化是最常用的?前序和后序线索化有什么局限性?
-
在线索二叉树中,如何找中序前驱节点?
-
线索二叉树的插入操作(如在节点p的右子树插入一个新节点)需要注意什么?
-
尝试实现前序线索化及其前序遍历。
欢迎在评论区讨论你的答案。