线索二叉树是一种特殊的二叉树,主要用于高效地实现树的遍历。与普通的二叉树相比,线索二叉树通过在节点中增加“线索”指针来简化遍历过程。值得注意的是,线索化二叉树的过程仍然需要使用递归,而后续遍历效率才会提高,适合一次构造,多次调用的场景。
前言
一般的二叉树在遍历时采用递归的方式,在深度比较大时效率不如线性存储的数据结构。对于一棵有n个结点二叉树如:,我们如果采用线性的方式表达它的先序排序(其他方式同理)会得到这样一个字符串:"ABDH#K###E##CFI###G#J##",其中‘#’表示留出一个空,如图中的结点H的左子树(实际上为空)。不难发现,将‘#’也看做是一个存储的信息时,一共会有2n个这样的信息需要被占用,但是由于‘#’没有实际含义,我们就猜想,能不能将‘#’的位置替换成某种其他信息,而这种信息可以记录这些结点之间的前驱后继关系?
答案是肯定的。另外,我们不难发现,连接这些结点一共需要n-1条边,也就是说只需要有n-1个额外的信息就可以将这些结点用线性关系全部链接起来。而实际上,被‘#’占用的信息一共有n+1个,也就是说,完全可以利用这些位置来存放表示结点之间的链式关系,而不用担心信息存放不下。我们暂且让第一个节点的前驱和最后一个结点的后继指向NULL。
有了以上结论,我们就可以开始重新构建结点的结构体来存放一些信息了。
一、结点结构体的重构
一般创建一棵普通二叉树,结点信息需要包含左子树、右子树和存放的数据:
// 定义树结点结构体,包含数据、左孩子、右孩子
typedef struct TreeNode{
Elemtype data;
struct TreeNode* lchild;
struct TreeNode* rchild;
}TreeNode;
而在线索二叉树中,除了表示左子树和右子树,还要有表示前驱结点和后继结点的信息,于是我们增加两个标志域,其结点形式如:
规定:ltag为0时表示lchild存放的是左子树,为1时表示lchild存放的是前驱结点;
rtag为0时表示rchild存放的是右子树,为1时表示rchild存放的是后继结点。
于是得到这样的结构体:
typedef char Elemtype;
typedef struct threadNode{
Elemtype data;
struct threadNode* lchild;
struct threadNode* rchild;
int ltag, rtag; // 用标识符表示此时的lchild和rchild分别表示的是子树还是前驱和后继
}threadNode;
typedef threadNode* threadTree; // 将threadNode类型的指针重命名为threadNode方便后续修改和遍历
注意到这里将threadNode类型的指针重命名为threadTree,方便后续修改和遍历传参。
二、树的创建
二叉树的创建有很多种模式,如按照前序、后序、中序、层序等排列方式来构建,原理类似,这里使用先序遍历为例,主要展示线索二叉树创建时ltag和rtag的赋值:
int ind; // 字符串下标
char tree[] = "ABDH#K###E##CFI###G#J##"; // 先序遍历的线性表示结果
// 树的创建
void* createTree(threadTree* T){
Elemtype ch = tree[ind++]; // 每次取出的字符,#表示为空
if(ch =='#'){
*T = NULL;
}
else{
*T = (threadTree)malloc(sizeof(threadTree)); // 分配内存并赋值给解引用指针后的T
(*T)->data = ch; // 传值
createTree(&(*T)->lchild); // 传入的仍然是指针
if((*T)->lchild != NULL){
(*T)->ltag = 0; // 左子树不为空的时候才能将其标识改成0,表示存放的是子树而不是前驱后继
}
createTree(&(*T)->rchild);
if((*T)->rchild != NULL){
(*T)->rtag = 0;
}
}
}
三、线索化
线索化的目的是将未利用的空间信息利用起来构建一个类似于双向循环链表的数据结构,所以我们首先要做的是,引入一个头结点head,让head的lchild指向根节点,rchild指向最后一个节点,让第一个结点的lchild指向头结点,最后一个节点的rchild指向头节点。注意这里的第一个和最后一个几点指的是按照指定的排序方式(如先序排序)第一个输出和最后一个输出的结点。
这样就能将所有的结点链接起来,线性访问。函数实现:
threadTree prve; // 全局变量记录上一个结点
// 线索化,要让头节点的lchild指向根节点,rchild指向最后一个结点
void inOrderThread(threadTree* head, threadTree T){
*head = (threadTree)malloc(sizeof(threadNode)); // 为头节点分配内存
(*head)->ltag = 0;
(*head)->rtag = 1; // lchild指向子树也就是根节点,rchild指向后继也就是尾结点
(*head)->rchild = *head; // 先指向头结点,如果头结点的后继指向自己,表示空树
if(T == NULL){
(*head)->lchild = *head; // 空树则指向自己
}
else{
(*head)->lchild = T; // 头结点的左孩子指向根节点
prve = *head; // 记录上一个节点
threading(T); // 具体的线索化
prve->rchild = *head; // 此时的prve在经过threading后已经变成了最后一个节点,故让其指向头结点head
prve->rtag = 1;
(*head)->rchild = prve; // 将头结点的右孩子指向prve最后一个节点
}
}
其中,我们调用了一个threading函数来进行具体的中间结点的线索化:
// 中序遍历具体添加线索
void threading(threadTree T){
// 当根结点不为空时直接访问左子树
if(T != NULL){
threading(T->lchild); // 左右根
if(T->lchild == NULL){
// 表示没有左子树,即有空位可以作为线索
T->ltag = 1; // 表示为前驱
T->lchild = prve; // 前驱为上一个节点,prve后面会更新
}
// 由于在遍历时无法找到下一个结点,无法直接将T->rchild找到
// 故要判断上一个节点也就是prve是否有右子树,这时候把prve的后继也就是当前节点T赋值给后继就行了
if(prve->rchild == NULL){
prve->rtag = 1; // 表示作为后继
prve->rchild = T; // T是prve的后继结点
}
// 当没有找到这样的有空位的结点时,更新prve
prve = T;
// 然后访问右子树
threading(T->rchild);
}
}
由于在确定后继节点时,无法根据当前节点来直接找到后继,所以采用对prve(上一个结点)添加后继(当前节点)来达到链接效果。
四、遍历线索二叉树
普通的二叉树遍历是这样的:
// 中序遍历
void inOrder(threadTree T){
if (T == NULL){
return ;
}
else{
inOrder(T->lchild);
printf("%c\n", T->data);
inOrder(T->rchild);
}
}
递归访问左子树和右子树,在深度较大时效率低。将二叉树线索化之后,通过头节点找到根节点,逐层的找到第一个输出的结点,然后根据链式关系遍历剩余的结点,直到遍历至根节点表示结束(线索化时已头尾相连),时间复杂度来到O(n):
// 中序线索化输出
void threadOrder(threadTree head){
// 传入的是链式结构的头结点,所以要先找到根节点再遍历左子树,直到找到最先输出的结点
threadTree curr = head->lchild; // 头结点的左子树指向的是根节点,rchild指向的是最后一个节点
while(curr!=head){
// 由于是循环链表,遍历之后一定会回到头结点,而当前T指向根节点,故选择curr=T作为终止循环条件
while(curr->ltag == 0){
// 一直遍历左子树直到有存放了前驱结点线索的结点(ltag=0)
curr = curr->lchild;
}
// 此时已经到了中序遍历第一个应该输出的结点
printf("%c\n", curr->data);
// 此时应该遍历后继结点
while(curr->rtag==1 && curr->rchild != head){
// 当rchild表示后继节点且后继节点不是头结点时,将更新curr并输出后继节点
curr = curr->rchild;
printf("%c\n", curr->data);
}
curr = curr->rchild; // 更新curr以便能跳出循环
}
}
测试代码:
int main(){
threadTree T;
threadTree head;
createTree(&T);
inOrder(T); // 普通中序遍历
printf("------------------\n");
inOrderThread(&head, T);
threadOrder(head); // 线索化后中序遍历
return 0;
}
输出结果对比: