数据结构(15.3)线索二叉树
前言
树形结构是一种非线性结构,我们对二叉树进行遍历,是将二叉树的结点按照一定的规则排列成一个线性序列,这实际上是对一个非线性化结构进行了线性化操作。前面提到过,线性结构除了第一个结点和最后一个结点以外,每一个结点都有且只有一个前驱和后继。观察我们的二叉树可以发现,我们没有是办法直接在树中找到每个结点的前驱和后继的,这个信息只能在二叉树的遍历中得到。
与此同时,二叉树中存在很多空的链域,造成了空间上的浪费。(有n个结点,就会有n+1个空链域)
为了解决这些问题,有人想到可以使用空的链域来存放结点的前驱和后继,于是线索二叉树就出现了。
现在,二叉树的子结点指向有两种可能性:既可能指向其左子树或者右子树,也可能指向其前驱或者后继。当结点指向的是前驱或者后继时,称结点为线索。拥有线索的二叉树就是线索二叉树。把空指针转化为线索的过程,就叫做线索化。
注意:将二叉树按照不同的规则来排列可以得到不同的序列,结点的前驱和后继也会不同,这样线索的指向也不同了。也就是说,对同一个二叉树,按照不同的次序进行线索化,可以分别得到先序线索二叉树、中序线索二叉树和后序线索二叉树。本文讲的是中序线索的二叉树。
线索二叉树的存储结构
我们知道,二叉树的结点有三个部分,分别是数据域、左孩子和右孩子。当使用空指针来存放线索时,我们不知道左右孩子指针指向的是子树还是线索。为了解决这个问题,线索化二叉树需要增加两个标志域,通过标志来协助判断。
//标记
typedef enum {
//子树
LINK,
//线索
THREAD
}TagType;
//线索二叉树的结点
typedef struct BinTreeNode{
//数据域
ElemType data;
//左孩子/线索
struct BinTreeNode *leftChild;
//右孩子/线索
struct BinTreeNode *rightChild;
//左标记
TagType leftTag;
//右标记
TagType rightTag;
}BinTreeNode;
线索二叉树的初始化与创建
线索化二叉树就是对二叉树里的空指针进行修改,使其指向结点前驱或者后继,而前驱和后继显然无法在创建时就获得。
这意味着要得到线索二叉树,首先要存在一个二叉树。因此在初始化和创建上,线索二叉树与二叉树是相同的。
创建是指通过输入一串字符串来生成二叉树,要有特殊的字符来表示结点不存在。因此初始化时要设置标记的值。
//初始化
void InitBinTree(BinTree *bt, ElemType ref){
//初始化树根为空
bt->root = NULL;
//设置结束标记
bt->refvalue = ref;
}
创建:
为了方便,先写一个生成结点的方法:
//生成一个结点
BinTreeNode *GetNewNode(ElemType x){
BinTreeNode *s = (BinTreeNode *)malloc(sizeof(BinTreeNode));
assert(s != NULL);
s->data = x;
s->leftChild = s->rightChild = NULL;
s->leftTag = s->rightTag = LINK;
return s;
}
然后实现创建方法:
//创建
void CreateBinTree(BinTree *bt, char *str){
CreateBinTree(bt, bt->root, str);
}
void CreateBinTree(BinTree *bt, BinTreeNode *&t, char *&str){
if (*str == bt->refvalue) {
t = NULL;
}else{
t = GetNewNode(*str);
CreateBinTree(bt, t->leftChild, ++str);
CreateBinTree(bt, t->rightChild, ++str);
}
}
注:这次的代码是用c++来写的,有些地方使用了引用,和直接用指针区别不大:
//创建
void CreateBinTree(BinTree *bt, char *str){
CreateBinTree1(bt, &(bt->root), str);
}
void CreateBinTree1(BinTree *bt, BinTreeNode **t,char *&str){
if (*str == bt->refvalue) {
t = NULL;
}else{
*t = GetNewNode(*str);
CreateBinTree1(bt, &(*t)->leftChild, ++str);
CreateBinTree1(bt, &(*t)->rightChild, ++str);
}
}
注注:字符串如果不使用引用,而是用char **str的话,我使用的编译器会出现一些问题,因此这里字符串还是使用的引用。
二叉树的中序线索化
来比较一下中序遍历方法和线索化方法:
中序遍历:
//中序遍历-递归
void InOrder(BinTreeNode *t){
if (t != NULL) {
//左子树递归遍历
InOrder(t->leftChild);
//输出结点信息
printf("%4c",t->data);
//右子树递归遍历
InOrder(t->rightChild);
}
}
线索化:
void CreateInThread(BinTreeNode *&t, BinTreeNode *&pre){
if (t != NULL) {
//左子树递归线索化
CreateInThread(t->leftChild, pre);
//将空指针变为线索
if (t ->leftChild == NULL) {
//左子树为空->左子树指向前驱结点
t->leftTag = THREAD;
t->leftChild = pre;
}
if (pre != NULL && pre->rightChild == NULL) {
//前驱结点的右子树为空->前驱结点的右子树指向该结点(后继)
pre->rightTag = THREAD;
pre->rightChild = t;
}
//更新前驱结点
pre = t;
//右子树递归线索化
CreateInThread(t->rightChild, pre);
}
}
可以发现,线索化方法和中序遍历方法在结构上是一致的,只不过是中间的操作有所区别。实际上,因为结点的前驱或后继信息只能在遍历中得到,中序线索化的实质就是在中序遍历的过程中,对空指针进行修改。
当我们找到一个子结点为空的结点时,假如是左结点,那么就要修改它的标记值,并且让它指向本结点的前驱。这说明了我们需要一个额外的变量来保存前驱结点。
假如是右结点,我们此时是不知道本结点的后继结点的。
但是注意:对本结点的前驱结点来说,本结点就是其后继结点。也就是说我们这时候能得到前驱结点的后继信息,因此可以判断前驱结点的右结点是否为空,假如为空,则修改它的标记值,然后让它指向本结点。
这时候本结点访问完毕了,需要更新一下前驱结点的指向(本结点成为了下一个结点的前驱结点)。这同样也说明,对结点的右结点进行修改,是要放到下一个结点中处理的。
有人可能注意到,本结点的右结点放到下一个结点来操作,那么到最后一个结点的右结点是不会被操作到的