一、前言:什么是二叉树的线索化?(为什么要有二叉树的线索化?)
- 通过前面内容的学习,我们知道了二叉树 的存储结构其实是通过二叉链表的方式实现的。但二叉链表由于每个结点均有左右孩子域,这使得叶子结点以及部分非叶子结点出现了孩子指针域为空的情况。
如图例:
- 可以发现的是,随着二叉树结点个数 的增长,二叉树中空指针域的个数越来越多,这会造成不小的存储空间浪费。
- 假设,现在需要你求解一个节点的前驱节点/后续节点,你要如何进行呢?
- 通过遍历方法我们确实可以知道二叉树中某个结点的前驱和后继分别是谁,但是这是建立在二叉树遍历的基础上的。一旦二叉树遍历结束后,如果想要知道二叉树中某个结点的前驱或后继时,得从头结点开始遍历二叉链表,这将会造成不小的时间开销。
- 如图中的二叉链表如果我想要找到 的前驱是谁,得遍历一遍二叉树才能搞定
- 既然有那么多空指针域,为什么不能把这些空指针域利用起来,让每个空指针域用来记录当前结点的前驱 呢?这样一来,是不是就解决了空指针域造成的存储空间浪费以及查找前驱后继时时造成的时间浪费呢?
因此,二叉线索树诞生了
二、二叉线索树的概念
2.1:名词解释
- 让二叉树中的空指针域记录二叉树某种遍历次序下的前驱和后继结点的地址,这样的话,再想找二叉树中某个结点的前驱后继时间性能将大为提升。
- 这种将二叉链表中指向前驱后继的指针称为线索 ,加上线索的二叉链表称之为线索链表 ,线索链表所对应的二叉树称之为线索二叉树(Threaded Binary Tree) 。
三、二叉线索树的实现
3.1:二叉线索树的基本数据结构
c
typedef struct ThreadedTreeNode { // 线索化二叉树节点
eleType val;
struct ThreadedTreeNode* left; // 左指针
struct ThreadedTreeNode* right; // 右指针
int leftTag; // 左标志位:0 表示指向左子树,1 表示指向前驱
int rightTag; // 右标志位:0 表示指向右子树,1 表示指向后继
} ThreadedTreeNode;
//leftTag和rightTag默认为0
eleType val
:存储节点的值,根据宏定义,这里是char
类型。 -struct ThreadedTreeNode* left
:指向左子节点的指针。struct ThreadedTreeNode* right
:指向右子节点的指针。int leftTag
:标志位,0
表示left
指针指向左子树,1
表示left
指针指向前驱节点。int rightTag
:标志位,0
表示right
指针指向右子树,1
表示right
指针指向后继节点。
3.2:初始化节点
c
// 初始化节点
ThreadedTreeNode* Init_Node(eleType val)
{
ThreadedTreeNode* node = (ThreadedTreeNode*)malloc(sizeof(ThreadedTreeNode));
node->val = val;
node->left = NULL;
node->right = NULL;
node->leftTag = 0;
node->rightTag = 0;
return node;
}
ThreadedTreeNode* Init_Node(eleType val)
:一个函数,用于初始化一个ThreadedTreeNode
节点。ThreadedTreeNode* node = (ThreadedTreeNode*)malloc(sizeof(ThreadedTreeNode));
:为新节点分配内存空间。node->val = val;
:将传入的值存储到节点的val
成员中。node->left = NULL;
和node->right = NULL;
:将左右子节点指针初始化为NULL
。node->leftTag = 0;
和node->rightTag = 0;
:将左右标志位初始化为0
,表示它们初始时都指向左右子树。return node;
:返回初始化好的节点指针。
3.3:创建一个普通二叉树
c
// 创建普通二叉树(同前面代码中一样,略去重复部分)
ThreadedTreeNode* CreateTree(eleType a[], int size, int index, eleType nullNode)
{
if (index >= size || a[index] == nullNode)
{
return NULL;
}
ThreadedTreeNode* root = Init_Node(a[index]); //初始化节点
root->left = CreateTree(a, size, 2 * index + 1, nullNode); //递归创建二叉树
root->right = CreateTree(a, size, 2 * index + 2, nullNode);
return root;
}
ThreadedTreeNode* CreateTree(eleType a[], int size, int index, eleType nullNode)
:一个递归函数,用于根据数组元素创建普通二叉树。if (index >= size || a[index] == nullNode)
:如果当前索引超出数组范围或元素等于nullNode
,返回NULL
,表示该位置无节点。ThreadedTreeNode* root = Init_Node(a[index]);
:创建当前位置的节点。root->left = CreateTree(a, size, 2 * index + 1, nullNode);
:递归创建左子树。root->right = CreateTree(a, size, 2 * index + 2, nullNode);
:递归创建右子树。return root;
:返回创建好的根节点。
3.4:通过中序遍历,线索化二叉树。
c
// 中序遍历线索化
ThreadedTreeNode* prev = NULL; // 全局变量记录前驱节点
void InThreading(ThreadedTreeNode* node)
{
if (node == NULL)
{
return;
}
// 线索化左子树
InThreading(node->left);
// 处理当前节点
if (node->left == NULL) // 如果左子树为空,建立前驱线索
{
node->left = prev;
node->leftTag = 1;
}
if (prev != NULL && prev->right == NULL) // 如果前驱的右子树为空,建立后继线索
{
prev->right = node;
prev->rightTag = 1;
}
prev = node; // 更新前驱为当前节点
// 线索化右子树
InThreading(node->right);
}
ThreadedTreeNode* prev = NULL;
:全局变量,用于记录中序遍历的前驱节点。void InThreading(ThreadedTreeNode* node)
:对二叉树进行中序线索化的函数。if (node == NULL) { return; }
:如果当前节点为空,返回。InThreading(node->left);
:先对左子树进行线索化。if (node->left == NULL) {...}
:如果当前节点的左子节点为空,将left
指针指向前驱节点(prev
),并将leftTag
置为1
。if (prev!= NULL && prev->right == NULL) {...}
:如果前驱节点的右子节点为空,将前驱节点的right
指针指向当前节点,并将rightTag
置为1
。prev = node;
:更新前驱节点为当前节点。InThreading(node->right);
:最后对右子树进行线索化。
通过中序遍历线索化的图示
3.5:遍历输出(线索化二叉树)
c
void InOrder_Threaded(ThreadedTreeNode* root)
{
// 从根节点开始遍历
ThreadedTreeNode* node = root;
while (node!= NULL)
{
// 找到最左边的节点,即中序遍历的起始节点
while (node->leftTag == 0)
{
node = node->left;
}
// 输出当前节点的值
printf("%c ", node->val);
// 若当前节点的右标志位为 1,表示存在后继线索
while (node->rightTag == 1)
{
// 按照后继线索移动到下一个节点
node = node->right;
// 输出该后继节点的值
printf("%c ", node->val);
}
// 进入右子树
node = node->right;
}
}
如图所示
void InOrder_Threaded(ThreadedTreeNode* root)
:这是一个函数,用于对线索化二叉树进行中序遍历。ThreadedTreeNode* node = root;
:首先,创建一个指针node
,并将其初始化为根节点root
,从根节点开始遍历。while (node!= NULL)
:只要node
不为空,就会继续遍历操作。while (node->leftTag == 0) { node = node->left; }
:此内层循环用于找到中序遍历的起始节点,即最左边的节点。只要当前节点的leftTag
为 0,意味着该节点有左子树,就将node
指针向左移动。当leftTag
不为 0 时,说明已经到达最左边的节点。printf("%c ", node->val);
:输出当前节点的值。while (node->rightTag == 1) {...}
:若当前节点的rightTag
为 1,说明当前节点有后继节点(通过线索化建立的后继线索)。在这个内层循环中,会不断沿着后继线索移动,并输出节点的值。node = node->right;
:当当前节点的rightTag
不为 1 时,意味着需要进入右子树,将node
指针移到右子树继续遍历。
- 这个函数利用了线索化二叉树的
leftTag
和rightTag
标志位,进行中序遍历。在正常的二叉树中序遍历中,我们通常需要使用栈或递归。 - 而在线索化二叉树中,通过
leftTag
找到最左边的节点,通过rightTag
找到后继节点,避免了栈或递归的使用,提高了遍历的效率。 - 需要注意的是,该函数其中
leftTag
为 0 表示指向左子树,为 1 表示指向前驱节点,rightTag
为 0 表示指向右子树,为 1 表示指向后继节点。这样,该函数可以沿着这些线索依次访问节点,完成中序遍历并输出节点的值。
3.5.1步骤解析:
以下是一个更详细的例子,帮助你理解 InOrder_Threaded
函数的工作原理。假设我们有以下线索化二叉树:
A
/ \
B C
/ \ \
D E F
假设我们已经对这个二叉树进行了中序线索化
(中序遍历结果为:D B E A C F)
在这个线索化二叉树中,假设 nil
表示 NULL
,并且线索化后的结构为:
D
的leftTag = 1
且left
指针指向前驱(这里假设为nil
,因为D
是第一个节点)。D
的rightTag = 1
且right
指针指向后继B
。B
的leftTag = 0
且left
指针指向左子树D
。B
的rightTag = 1
且right
指针指向后继E
。E
的leftTag = 1
且left
指针指向前驱B
。E
的rightTag = 1
且right
指针指向后继A
。A
的leftTag = 0
且left
指针指向左子树B
。A
的rightTag = 0
且right
指针指向右子树C
。C
的leftTag = 0
且left
指针指向左子树(这里假设为nil
)。C
的rightTag = 1
且right
指针指向后继F
。F
的leftTag = 1
且left
指针指向前驱C
。F
的rightTag = 1
且right
指针指向后继(这里假设为nil
,因为F
是最后一个节点)。
以下是使用 InOrder_Threaded
函数对上述线索化二叉树进行中序遍历的步骤:
c
// 中序遍历线索化二叉树
void InOrder_Threaded(ThreadedTreeNode* root)
{
ThreadedTreeNode* node = root;
while (node!= NULL)
{
// 找到最左边的节点
while (node->leftTag == 0)
{
node = node->left;
}
// 输出当前节点的值
printf("%c ", node->val);
// 按照后继线索遍历
while (node->rightTag == 1)
{
node = node->right;
printf("%c ", node->val);
}
// 进入右子树
node = node->right;
}
}
解释上述代码:
- 中序遍历线索化二叉树 :
- 调用
InOrder_Threaded
函数,从根节点A
开始遍历。 - 首先,
node
指针从A
开始,通过while (node->leftTag == 0) { node = node->left; }
找到最左边的节点D
。 - 输出
D
的值,因为D
的rightTag
为 1,会继续沿着后继线索输出B
的值。 - 由于
B
的rightTag
为 1,继续输出E
的值。 - 因为
E
的rightTag
为 1,输出A
的值。 - 此时
A
的rightTag
为 0,进入A
的右子树,继续上述过程,找到C
,输出C
的值。 - 因为
C
的rightTag
为 1,输出F
的值。
- 调用
当运行 InOrder_Threaded(A)
时,会输出:D B E A C F
,这是因为中序遍历的顺序是先左子树,然后根节点,最后右子树,而线索化二叉树通过线索方便地找到了前驱和后继,避免了递归或栈的使用。
在 InOrder_Threaded
函数中:
while (node!= NULL)
:确保遍历不会超出二叉树范围。while (node->leftTag == 0) { node = node->left; }
:不断向左移动,找到最左边的节点。printf("%c ", node->val);
:输出当前节点的值。while (node->rightTag == 1) { node = node->right; printf("%c ", node->val); }
:如果存在后继线索,沿着后继线索输出节点。node = node->right;
:如果没有后继线索,进入右子树。
通过这个例子,你可以更清楚地看到 InOrder_Threaded
函数如何利用线索化二叉树的特性,不使用递归或栈,实现中序遍历。
c
int main() {
const char nullNode = '*';
char a[15] = {
'-', '+', '/', 'a', 'x', 'e', 'f', '*', '*', 'b', '-', '*', '*', 'c', 'd'
};
ThreadedTreeNode* root = CreateTree(a, 15, 0, nullNode);
InThreading(root);
printf("中序遍历线索化二叉树:");
InOrder_Threaded(root);
printf("\n");
system("pause");
return 0;
}
int main()
:程序的入口函数。const char nullNode = '*';
:定义一个表示空节点的字符。char a[15] = {...};
:存储二叉树节点元素的数组。ThreadedTreeNode* root = CreateTree(a, 15, 0, nullNode);
:根据数组元素创建二叉树。InThreading(root);
:对创建好的二叉树进行线索化。printf("中序遍历线索化二叉树:");
:输出提示信息。InOrder_Threaded(root);
:中序遍历线索化二叉树并输出节点值。system("pause");
:暂停程序,等待用户按键(Windows 平台特有)。return 0;
:程序正常结束,返回0
。
这段代码的主要功能是创建一个普通二叉树,对其进行中序线索化,然后使用中序线索化后的结构进行中序遍历。线索化二叉树是一种特殊的二叉树结构,通过 leftTag
和 rightTag
这两个标志位,可以将原本为空的左右子节点指针利用起来,分别指向前驱和后继节点,这样在中序遍历的时候,不需要使用栈或递归就可以实现,提高了遍历的效率。对于存储的数据类型,这里使用 char
作为元素类型,但可以通过修改 eleType
的宏定义来改变存储的数据类型。对于二叉树的创建,使用了一个数组来存储元素,通过 CreateTree
函数递归构建二叉树,使用 nullNode
来表示空节点。线索化过程使用 InThreading
函数,通过 prev
变量记录前驱节点并修改节点的指针和标志位,最后使用 InOrder_Threaded
函数进行中序遍历,利用线索进行高效的遍历操作。
完整参考代码实现:
c
#include <stdio.h>
#include <stdlib.h>
#define eleType char
typedef struct ThreadedTreeNode { // 线索化二叉树节点
eleType val;
struct ThreadedTreeNode* left; // 左指针
struct ThreadedTreeNode* right; // 右指针
int leftTag; // 左标志位:0 表示指向左子树,1 表示指向前驱
int rightTag; // 右标志位:0 表示指向右子树,1 表示指向后继
} ThreadedTreeNode;
//leftTag和rightTag默认为0
// 初始化节点
ThreadedTreeNode* Init_Node(eleType val)
{
ThreadedTreeNode* node = (ThreadedTreeNode*)malloc(sizeof(ThreadedTreeNode));
node->val = val;
node->left = NULL;
node->right = NULL;
node->leftTag = 0;
node->rightTag = 0;
return node;
}
// 创建普通二叉树(同前面代码中一样,略去重复部分)
ThreadedTreeNode* CreateTree(eleType a[], int size, int index, eleType nullNode)
{
if (index >= size || a[index] == nullNode)
{
return NULL;
}
ThreadedTreeNode* root = Init_Node(a[index]); //初始化节点
root->left = CreateTree(a, size, 2 * index + 1, nullNode); //递归创建二叉树
root->right = CreateTree(a, size, 2 * index + 2, nullNode);
return root;
}
// 中序遍历线索化
ThreadedTreeNode* prev = NULL; // 全局变量记录前驱节点
void InThreading(ThreadedTreeNode* node)
{
if (node == NULL)
{
return;
}
// 线索化左子树
InThreading(node->left);
// 处理当前节点
if (node->left == NULL) // 如果左子树为空,建立前驱线索
{
node->left = prev;
node->leftTag = 1;
}
if (prev != NULL && prev->right == NULL) // 如果前驱的右子树为空,建立后继线索
{
prev->right = node;
prev->rightTag = 1;
}
prev = node; // 更新前驱为当前节点
// 线索化右子树
InThreading(node->right);
}
// 中序遍历线索化二叉树
void InOrder_Threaded(ThreadedTreeNode* root)
{
ThreadedTreeNode* node = root;
while (node != NULL)
{
// 找到第一个左子树不为空的节点
while (node->leftTag == 0)
{
node = node->left;
}
// 输出当前节点
printf("%c ", node->val);
// 按照后继线索遍历
while (node->rightTag == 1)
{
node = node->right;
printf("%c ", node->val);
}
// 进入右子树
node = node->right;
}
}
int main() {
//const char nullnode = '*';
//char a[24] = {
// nullnode,'-','+','/','a', //4
// 'x','e','f',nullnode,nullnode, //9
// 'b','-',nullnode,nullnode,nullnode, //14
// nullnode,nullnode,nullnode,nullnode,nullnode,//19
// nullnode,nullnode,'c','d' //23
//};
const char nullNode = '*';
char a[15] = {
'-', '+', '/', 'a', 'x', 'e', 'f', '*', '*', 'b', '-', '*', '*', 'c', 'd'
};
// 创建普通二叉树
ThreadedTreeNode* root = CreateTree(a, 15, 0, nullNode);
// 线索化二叉树
InThreading(root);
// 中序遍历线索化二叉树
printf("中序遍历线索化二叉树:");
InOrder_Threaded(root);
printf("\n");
system("pause");
return 0;
}