线索二叉树的思想来源于二叉树的存储结构中,存在一些空的指针域,因此是否能够将这些空间利用起来,存储一些关于节点间先后顺序的信息,由此产生了线索二叉树。线索二叉树中,线索反映前驱、后继的关系,而指针则体现左右子树。
以二叉链表为例,线索二叉树存储结构上的特点是添加标识符,表明左右指针域究竟存的是指向前驱和后继的线索,还是指向左右子树的指针;
线索二叉树的优势是一旦对一棵二叉树建立了相应的线索结构,当以后使用特定的方式遍历这课树时,可以避免递归方式和非递归方式(利用栈)带来的空间开销。
构建和遍历线索二叉树的思路如下:
首先构造一棵二叉树(构造二叉树可以使用任意顺序:先序、中序、后续均可);
按照一定的顺序线索化这棵二叉树
然后按照相同的顺序遍历该二叉树,就可以利用上一步构建的线索信息。
这里以带线索的二叉链表方式作为存储结构,先序创建一棵二叉树,然后中序线索化这棵二叉树,得到的是一棵中序线索二叉树,此后再中序遍历该二叉树,就可以使用这里的线索信息,不用额外的栈空间。
1. biThrTree.h,节点存储结构
#include<stdio.h>
#include<stdlib.h>
#define OK 1
#define ERROR 0
#define OVERFLOW -1
typedef int Status;
typedef char TElemType;
typedef enum {Link, Thread} PointerTag;
typedef struct BiThrNode{
TElemType data;
PointerTag LTag, RTag; // 枚举型的特点,LTag和RTag初始为0,即Link
struct BiThrNode *lchild, *rchild;
}BiThrNode, *BiThrTree;
BiThrTree pre = NULL; // 在线索化二叉树中用到,记录遍历过程中,一个节点的前驱
由于枚举型的变量声明是如果没有赋初值,默认设置初值为0,所以等于默认每个BiThrNode实例的LTag和RTag都是Link。
2. biThrTree.h,简单的遍历操作——打印
Status PrintElement(TElemType e){
printf("%c", e);
return OK;
}
``
3. biThrTree.h,先序创建二叉树
`
创建二叉树可以采用任何顺序,这里以先序创建为例。
```cpp
BiThrTree CreateBiThrTree()
{
TElemType ch;
BiThrTree T;
scanf("%c", &ch);
getchar(); // “吃掉”每次输入到缓冲区中的回车,不能缺少
if (ch == '$')
T = NULL;
else{
T = (BiThrTree)malloc(sizeof(BiThrNode));
if(!T)
exit(OVERFLOW);
T->data = ch; // 11 - 14行代码与下面两个递归调用的位置体现了创建函数采用的遍历顺序
printf("Input lchild of %c:\n", ch);
T->lchild = CreateBiThrTree();
printf("Input rchild of %c:\n", ch);
T->rchild = CreateBiThrTree();
}
return T;
}
这里以美元符号"$"表示空指针,告诉程序一个节点没有左子树或右子树。
创建一棵二叉树与单纯以某种顺序写出二叉树的遍历结果不同,在创建二叉树时,要以某个符号指定NULL指针,这样程序才知道哪里是叶子节点,哪里不能再向左/右,必须回退了。
具体的例子在本文最后的运行示例中给出
4. biThrTree.h,中序线索化已经创建的二叉树
线索化针对的是已经创建好的二叉树,将其中的空指针域利用起来指向前驱后继的过程就是线索化。
void InThreading(BiThrTree p){
if(p){
InThreading(p->lchild);
if(!(p->lchild)){
p->LTag = Thread;
p->lchild = pre;
}
if(!(pre->rchild)){
pre->RTag = Thread;
pre->rchild = p;
}
pre = p;
InThreading(p->rchild);
}
}
Status InOrderThreading(BiThrTree *Thrt, BiThrTree T){
(*Thrt) = (BiThrTree)malloc(sizeof(BiThrNode)); //头节点不同于二叉树的根节点
if(!(*Thrt))
exit(OVERFLOW);
(*Thrt)->RTag = Thread;
(*Thrt)->rchild = *Thrt;
(*Thrt)->LTag = Link;
if(!T)
(*Thrt)->lchild = *Thrt;
else{
(*Thrt)->lchild = T;
pre = *Thrt; // 全局的pre,这里初始化指向头结点
InThreading(T);
pre->rchild = *Thrt; // 此时pre停留在中序遍历二叉树时的最后一个节点上
pre->RTag = Thread;
(*Thrt)->rchild = pre;
}
return OK;
}
线索化只针对原来是空的指针域,一个节点如果左指针域为空,那么线索化后该指针域指向其遍历过程中的前驱;类似的,如果其右指针域为空,线索化后右指针域指向其遍历过程的后继。线索化后的二叉链树中不存在空的指针域。
考虑这段代码中的 4 -12 行,一方面线索化的过程只关注已经创建好的二叉树中那些空的指针域;另一方面,上文关于存储结构的介绍已经提到,LTag和RTag的初始值都是Link,所以当中序遍历到达某个节点时,非空的指针域已经不需要再设置Link。
4 -12 行代码中,对于中序遍历到的每一个节点,如果它的左指针域为空,那么我们在这次遍历中将其置为线索,而如果它的右指针域为空,我们在遍历走向下一个节点时线索化上一个节点的右指针域。
这样带来一个问题,一棵树中最后一个被遍历到的节点的右指针域没有被线索化,这也就是 31 - 33 行代码的工作。
InOrderThreading() 创建一个额外的头结点 Thrt ,头结点不同于二叉树的根节点。头结点用来解决遍历二叉树过程中,第一个遍历到的节点没有前驱和最后一个遍历到的节点没有后继的问题。
对于一棵非空的二叉树,Thrt的LTag为Link,左指针指向二叉树的根节点;遍历二叉树时的第一个节点,如果左指针域为空,则将其指向Thrt。Thrt的RTag为Thread,Thrt的右指针指向遍历二叉树时的最后一个节点,同时如果最后一个节点的右指针域为空,则将其指向Thrt。
从而中序线索化时,将会形成一个从Thrt出发,再回到Thrt的闭环,这也是线索化的优势。
5. biThrTree.h,中序遍历已经线索化的二叉树
采用某种顺序线索化的二叉树,可以相应地采用同样的顺序进行遍历,中序遍历中序线索二叉树,不需要额外的栈空间支持,根据线索即可完成遍历,适用于需要经常对树进行遍历操作,或者给定某个节点,需要判断其前驱或者后继的情境。
Status InOrderTraverse_Thr( BiThrTree Thrt, Status (*Visit)(TElemType e)){
BiThrTree p;
p = Thrt->lchild;
while(p != Thrt){
while(p->LTag == Link)
p = p->lchild; //先走到最左端,开始
if(!(*Visit)(p->data))
return ERROR;
while(p->RTag == Thread && p != Thrt){
p = p->rchild;// 如果右指针是个线索,直接向后遍历后继
(*Visit)(p->data);
}
if (p != Thrt) // 如果不加区分就走向右子树,很有可能出现死循环,因为 p刚刚停到Thrt,还没进行下一次循环判断,就立刻指向Thrt的后继,循环可能不会终止
p = p->rchild;
}
return OK;
线索化和利用线索的遍历必须遵循相同的顺序。
#include"biThrTree.h"
// 1. Create Binary Tree with Pre-Order input(Creation can be in any order)
// 2. In-Order Threading this tree (How to threading the tree depends on in what
// order you are goning to traverse the tree.)
// 3. then In-Order Traverse this tree
int main(int argc, char *argv[]){
BiThrTree T, temp;
printf("Creating Binary Thread Tree.\n");
T = CreateBiThrTree();
if(!T)
return OVERFLOW;
printf("Binary Thread Tree Created.\n");
if(!InOrderThreading(&temp, T))
return ERROR;
printf("In Order Traversing the Binary Thread Tree:\n");
if(!InOrderTraverse_Thr(temp, &PrintElement))
return ERROR;
printf("\nIn Order Traversing Accomplished.\n");
return OK;
}
主程序首先创建一棵二叉树,然后对其进行中序线索化,最后中序遍历,打印遍历结果。
下面以输入:
ABC$$DE$G$$F$$$
为例先序构建一棵二叉树,这棵二叉树的样子如下:
A
/
B
/ \
C D
/ \
E F
\
G
在构建二叉树时,必须指定空指针的位置。
在GCC下编译,程序执行的过程如下:
./a.out
Creating Binary Thread Tree.
A
Input lchild of A:
B
Input lchild of B:
C
Input lchild of C:
$
Input rchild of C:
$
Input rchild of B:
D
Input lchild of D:
E
Input lchild of E:
$
Input rchild of E:
G
Input lchild of G:
$
Input rchild of G:
$
Input rchild of D:
F
Input lchild of F:
$
Input rchild of F:
$
Input rchild of A:
$
Binary Thread Tree Created.
In Order Traversing the Binary Thread Tree.
CBEGDFA
In Order Traversing Accomplished.
最后介绍一个中序线索化二叉树的优点,即中序线索化后,对于二叉树中的任何一个节点,我们都可以快速地求它的前驱和后继:
前驱:
左指针是Thread,lchild就是前驱
左指针是Link,lchild是其左子树,但是可以找到前驱:中序遍历是先左子树,对于左子树而言,最后遍历左子树的右子树,所以此时一个节点的前驱是其左子树最右下的节点,沿着左子树的右Link一直向下,遇到RTag为Thread的就是目标节点的前驱
后继:
右指针是Thread,则rchild就是后继
右指针是Link,rchild是右子树,但是可以找到后继:中序遍历时,后遍历某个节点的右子树,而该节点的后继就是其右子树中最先被遍历到的,即其右子树最左下的节点,沿着右子树的左Link一直向左下,直到遇到LTag为Thread的就是目标节点的后继。
所以,中序线索化还是很便于索引树中任意一个节点的前驱后继关系的。