线索二叉树:非递归中序遍历的实现与原理
在数据结构中,二叉树是一种常见且功能强大的树形结构。然而,对于普通二叉树的遍历(如前序、中序、后序遍历),通常需要借助栈(递归本质上也是利用了系统栈)来存储节点信息,以便在回溯时访问父节点或兄弟节点。当树的深度较大时,这可能导致栈溢出或额外的空间开销。为了优化这一问题,线索二叉树应运而生。
线索二叉树的核心思想是利用二叉树中大量存在的空指针。在一个具有 N 个节点的二叉链表中,总共有 2N 个指针域。如果采用二叉链表作为存储结构,那么 N 个节点将有 N-1 条指向子节点的非空指针,因此会有 2N - (N-1) = N+1 个空指针域。线索二叉树正是通过这些空指针域,建立指向节点前驱和后继的指针,从而避免在遍历时使用栈,实现O(1)额外空间复杂度的线性遍历。
线索二叉树节点结构
为了区分一个指针是指向子节点还是指向前驱/后继节点(即"线索"),我们需要在每个节点中增加两个标志位。典型的线索二叉树节点结构如下:
typedef char data_elem_t; // 节点数据类型
typedef struct thread_node {
data_elem_t data; // 节点数据
struct thread_node *left; // 左子节点或前驱节点
struct thread_node *right; // 右子节点或后继节点
int l_tag; // 左标志:0表示left指向左子树,1表示left指向前驱节点
int r_tag; // 右标志:0表示right指向右子树,1表示right指向后继节点
} ThreadNode;
typedef struct {
ThreadNode *root; // 树的根节点
int node_count; // 节点数量
} ThreadedBinaryTree;
其中,l_tag 和 r_tag 是关键。当 l_tag 为 0 时,left 指针与普通二叉树一样,指向左子节点;当 l_tag 为 1 时,left 指针指向其中序遍历下的前驱节点。同理,r_tag 控制 right 指针是指向右子节点还是其中序遍历下的后继节点。
线索化过程
线索二叉树的构建分为两个主要阶段:
- 构建普通二叉树: 按照常规方式创建二叉树,并初始化节点的
left和right指针以及l_tag和r_tag(通常全部设为0,即默认指向子树)。 - 线索化: 对已构建的二叉树进行一次遍历(通常是中序遍历),在遍历过程中,将所有为空的
left指针连接到其前驱节点,将所有为空的right指针连接到其后继节点,并相应地设置l_tag和r_tag为 1。
以下图为例展示线索化后的结构:

在进行中序线索化时,我们需要一个辅助指针来跟踪上一个被访问的节点。当当前节点的 left 指针为空时,它应该指向前一个被访问的节点;当前一个节点的 right 指针为空时,它应该指向当前节点。
static ThreadNode *g_last_visited_node = NULL; // 用于线索化时跟踪前一个访问的节点
// 递归地进行中序线索化
static void internalInOrderThreading(ThreadNode *node) {
if (node == NULL) {
return;
}
// 递归线索化左子树
internalInOrderThreading(node->left);
// 处理当前节点:
// 如果当前节点的左指针为空,则将其线索化指向前驱
if (node->left == NULL) {
node->l_tag = 1;
node->left = g_last_visited_node;
}
// 如果前一个节点的右指针为空,则将其线索化指向当前节点
if (g_last_visited_node != NULL && g_last_visited_node->right == NULL) {
g_last_visited_node->r_tag = 1;
g_last_visited_node->right = node;
}
// 更新g_last_visited_node为当前节点
g_last_visited_node = node;
// 递归线索化右子树
internalInOrderThreading(node->right);
}
// 对整个线索二叉树进行中序线索化
void threadTreeInOrder(ThreadedBinaryTree *tree) {
if (tree == NULL || tree->root == NULL) {
return;
}
g_last_visited_node = NULL; // 每次线索化前重置
internalInOrderThreading(tree->root);
// 处理线索化结束后,最后一个节点的后继指针。
// 如果最后一个节点的right指针仍为空,将其设为NULL并标记为线索
if (g_last_visited_node != NULL && g_last_visited_node->right == NULL) {
g_last_visited_node->r_tag = 1;
g_last_visited_node->right = NULL;
}
}
中序遍历线索二叉树
线索二叉树的最大优势在于,一旦完成线索化,就可以通过非递归方式进行线性遍历,无需栈的支持。中序遍历线索二叉树的步骤如下:
- 从根节点开始,找到中序遍历的第一个节点(即沿着左子树一直走到最左端的节点)。
- 访问该节点。
- 如果该节点的
r_tag为 1,则其right指针指向的就是中序后继,直接移动到该后继节点。 - 如果该节点的
r_tag为 0,则其right指针指向的是右子树。此时,我们需要找到右子树中中序遍历的第一个节点(即右子树的最左端节点)。 - 重复步骤 2-4,直到所有节点都被访问,或到达根节点右子树的最右端节点的后继为 NULL。
void traverseThreadedTreeInOrder(ThreadedBinaryTree *tree) {
if (tree == NULL || tree->root == NULL) {
return;
}
ThreadNode *currentNode = tree->root;
// 1. 找到中序遍历的第一个节点(最左子节点)
while (currentNode != NULL && currentNode->l_tag == 0) {
currentNode = currentNode->left;
}
// 2. 开始遍历
while (currentNode != NULL) {
printNodeData(currentNode); // 访问当前节点
// 如果当前节点的右指针是线索,直接跳到后继节点
if (currentNode->r_tag == 1) {
currentNode = currentNode->right;
} else {
// 如果右指针是子树,则找到右子树中中序遍历的第一个节点
currentNode = currentNode->right;
while (currentNode != NULL && currentNode->l_tag == 0) {
currentNode = currentNode->left;
}
}
}
printf("\n"); // 遍历结束后换行
}
完整的代码示例
thread_tree.h
#ifndef THREAD_TREE_H
#define THREAD_TREE_H
#include <stdbool.h> // For bool type
typedef char data_elem_t;
typedef struct thread_node {
data_elem_t data;
struct thread_node *left;
struct thread_node *right;
int l_tag; // 0: left child, 1: predecessor
int r_tag; // 0: right child, 1: successor
} ThreadNode;
typedef struct {
ThreadNode *root;
int node_count;
} ThreadedBinaryTree;
// 创建并初始化一个空的线索二叉树
ThreadedBinaryTree *createThreadedTree();
// 释放线索二叉树所有节点内存
void destroyThreadedTree(ThreadedBinaryTree *tree);
// 初始化线索二叉树的根节点
void initializeThreadedTreeRoot(ThreadedBinaryTree *tree, ThreadNode *root_node);
// 创建一个新节点
ThreadNode *createThreadNode(data_elem_t data);
// 为父节点添加左右子节点 (注意:这里假定左右子节点一开始不是线索)
void addChildNodes(ThreadedBinaryTree *tree, ThreadNode *parent, ThreadNode *left_child, ThreadNode *right_child);
// 打印节点数据
void printNodeData(ThreadNode *node);
// 中序线索化二叉树
void threadTreeInOrder(ThreadedBinaryTree *tree);
// 中序遍历线索化后的二叉树
void traverseThreadedTreeInOrder(ThreadedBinaryTree *tree);
#endif // THREAD_TREE_H
thread_tree.c
#include "thread_tree.h"
#include <stdio.h>
#include <stdlib.h>
// 创建并初始化一个空的线索二叉树
ThreadedBinaryTree *createThreadedTree() {
ThreadedBinaryTree *tree = (ThreadedBinaryTree *)malloc(sizeof(ThreadedBinaryTree));
if (tree == NULL) {
perror("Failed to allocate memory for ThreadedBinaryTree");
exit(EXIT_FAILURE);
}
tree->node_count = 0;
tree->root = NULL;
return tree;
}
// 初始化线索二叉树的根节点
void initializeThreadedTreeRoot(ThreadedBinaryTree *tree, ThreadNode *root_node) {
if (tree != NULL && tree->root == NULL && root_node != NULL) {
tree->root = root_node;
tree->node_count = 1;
} else if (root_node == NULL) {
fprintf(stderr, "Error: Cannot initialize tree with a NULL root node.\n");
} else if (tree->root != NULL) {
fprintf(stderr, "Warning: Tree already has a root. Not re-initializing.\n");
}
}
// 创建一个新节点
ThreadNode *createThreadNode(data_elem_t data) {
ThreadNode *node = (ThreadNode *)malloc(sizeof(ThreadNode));
if (node == NULL) {
perror("Failed to allocate memory for ThreadNode");
exit(EXIT_FAILURE);
}
node->data = data;
node->left = NULL;
node->l_tag = 0; // 默认指向子节点
node->right = NULL;
node->r_tag = 0; // 默认指向子节点
return node;
}
// 为父节点添加左右子节点 (注意:这里假定左右子节点一开始不是线索)
void addChildNodes(ThreadedBinaryTree *tree, ThreadNode *parent, ThreadNode *left_child, ThreadNode *right_child) {
if (tree == NULL || parent == NULL) {
fprintf(stderr, "Error: Parent or tree is NULL for addChildNodes.\n");
return;
}
if (left_child != NULL) {
parent->left = left_child;
tree->node_count++;
}
if (right_child != NULL) {
parent->right = right_child;
tree->node_count++;
}
}
// 打印节点数据
void printNodeData(ThreadNode *node) {
if (node != NULL) {
printf("%c ", node->data);
}
}
// 全局静态指针,用于在递归线索化过程中跟踪上一个被访问的节点
static ThreadNode *g_last_visited_node = NULL;
// 递归地进行中序线索化
static void internalInOrderThreading(ThreadNode *node) {
if (node == NULL) {
return;
}
// 递归线索化左子树
internalInOrderThreading(node->left);
// 处理当前节点:
// 如果当前节点的左指针为空,则将其线索化指向前驱
if (node->left == NULL) {
node->l_tag = 1;
node->left = g_last_visited_node;
}
// 如果前一个节点的右指针为空,则将其线索化指向当前节点
if (g_last_visited_node != NULL && g_last_visited_node->right == NULL) {
g_last_visited_node->r_tag = 1;
g_last_visited_node->right = node;
}
// 更新g_last_visited_node为当前节点
g_last_visited_node = node;
// 递归线索化右子树
internalInOrderThreading(node->right);
}
// 对整个线索二叉树进行中序线索化
void threadTreeInOrder(ThreadedBinaryTree *tree) {
if (tree == NULL || tree->root == NULL) {
return;
}
g_last_visited_node = NULL; // 每次线索化前重置
internalInOrderThreading(tree->root);
// 处理线索化结束后,最后一个节点的后继指针。
// 如果最后一个节点的right指针仍为空,将其设为NULL并标记为线索
if (g_last_visited_node != NULL && g_last_visited_node->right == NULL) {
g_last_visited_node->r_tag = 1;
g_last_visited_node->right = NULL;
}
}
// 中序遍历线索化后的二叉树
void traverseThreadedTreeInOrder(ThreadedBinaryTree *tree) {
if (tree == NULL || tree->root == NULL) {
return;
}
ThreadNode *currentNode = tree->root;
// 1. 找到中序遍历的第一个节点(最左子节点)
while (currentNode != NULL && currentNode->l_tag == 0) {
currentNode = currentNode->left;
}
// 2. 开始遍历
while (currentNode != NULL) {
printNodeData(currentNode); // 访问当前节点
// 如果当前节点的右指针是线索,直接跳到后继节点
if (currentNode->r_tag == 1) {
currentNode = currentNode->right;
} else {
// 如果右指针是子树,则找到右子树中中序遍历的第一个节点
currentNode = currentNode->right;
while (currentNode != NULL && currentNode->l_tag == 0) {
currentNode = currentNode->left;
}
}
}
printf("\n"); // 遍历结束后换行
}
// 辅助函数:递归销毁节点
static void recursiveDestroyNode(ThreadNode *node) {
if (node == NULL) {
return;
}
// 只有当指针指向子树时才递归销毁,避免沿着线索重复销毁或销毁外部节点
if (node->l_tag == 0) {
recursiveDestroyNode(node->left);
}
if (node->r_tag == 0) {
recursiveDestroyNode(node->right);
}
free(node);
}
// 释放线索二叉树所有节点内存
void destroyThreadedTree(ThreadedBinaryTree *tree) {
if (tree == NULL) {
return;
}
recursiveDestroyNode(tree->root);
//printf("\nDestroyed %d nodes.\n", tree->node_count); // Note: node_count might not be accurate after threading
free(tree);
}
main.c
#include "thread_tree.h"
#include <stdio.h>
#include <stdlib.h>
// 构建一个测试用的二叉树
ThreadedBinaryTree *buildTestTree() {
ThreadedBinaryTree *tree = createThreadedTree();
ThreadNode *nodeA = createThreadNode('A');
ThreadNode *nodeB = createThreadNode('B');
ThreadNode *nodeC = createThreadNode('C');
ThreadNode *nodeD = createThreadNode('D');
ThreadNode *nodeE = createThreadNode('E');
ThreadNode *nodeF = createThreadNode('F');
ThreadNode *nodeG = createThreadNode('G');
ThreadNode *nodeH = createThreadNode('H');
ThreadNode *nodeK = createThreadNode('K');
initializeThreadedTreeRoot(tree, nodeA);
// 建立树的结构
addChildNodes(tree, nodeA, nodeB, nodeE);
addChildNodes(tree, nodeB, NULL, nodeC);
addChildNodes(tree, nodeC, nodeD, NULL);
addChildNodes(tree, nodeE, NULL, nodeF);
addChildNodes(tree, nodeF, nodeG, NULL);
addChildNodes(tree, nodeG, nodeH, nodeK);
return tree;
}
int main() {
ThreadedBinaryTree *myTree = buildTestTree();
printf("初始树节点数量: %d\n", myTree->node_count);
printf("开始中序线索化...\n");
threadTreeInOrder(myTree);
printf("线索化完成。\n");
printf("中序遍历线索二叉树: ");
traverseThreadedTreeInOrder(myTree);
destroyThreadedTree(myTree);
printf("树已销毁。\n");
return 0;
}
运行结果
初始树节点数量: 9
开始中序线索化...
线索化完成。
中序遍历线索二叉树: B D C A H G K F E
树已销毁。