线索二叉树概述
二叉树虽然是非线性结构,但二叉树的遍历却为二叉树的结点集导出了一个线性序列。如果我们希望很快找到某一结点的前驱或后继,但不希望每次都要对二叉树遍历一遍,这就需要把每个结点的前驱和后继信息记录下来。
为了做到这一点,可在原来的二叉链表中增加一个前驱指针域(pred)和一个后继指针域(succ),分别指向该结点在某种次序下的前驱结点和后继结点,但是这种方法将会导致很多结点中有没有利用的空指针,先看下面的例子。
比如:现在有一个结点数为 n 的二叉树,采用二叉链表形式存储。对于每个结点均有指向左右孩子的指针域,可以得出:结点数为 n 的二叉树共有 n - 1 条有效分支路径。而且二叉树中存在 2n - (n - 1) = n + 1 个空指针域。

此二叉树有 6 个结点,存在 7 个空指针域。该二叉树的中序遍历为:CBEDFA,从遍历的结果输出中,可以轻松直到 B 结点的前驱结点为 C,后继结点为 E。但是这种关系是在建立好二叉树的基础上进行遍历得到的,是否可以在建立二叉树的时候,就记录下前驱后继的关系呢,如果可以,在后续寻找任意结点去寻找前驱结点和后继结点时将大大提升效率。
线索化
上面说过为每个结点增加一个前驱指针域和一个后继指针域,其实大可不必。完全可以将利用这些空的指针域,将其指向前驱或后继,定义规则如下:
如果结点左子树为空,则该结点的左孩子指向其前驱结点。
如果结点右子树为空,则该结点的右孩子指向其后继结点。
这种指向前驱和后继的指针称为线索,将一刻二叉树以某种次序遍历,并添加线索的过程称为线索化。
线索化即解决了空间的浪费和前驱后继的记录问题。
那么问题来了,我们如何区分一个结点的 lchild 指针是指向左孩子还是指向前驱结点呢?
为了解决这个问题,区别线索和子女指针,在每个结点中设置两个标志 ltag 和 rtag。以中序线索二叉树为例,如果 ltag == 0,标明 leftChild 域中存放的是指向左子女结点的指针,否则 leftChild 域中是指向该结点中序下的前驱的线索;如果 rtag == 0,标明 rightChild 域中存放的是指向右子女结点的指针,否则 rightChild 域中是指向该结点中序下的后继的线索。由于标记位只需占用一个二进位,每个结点所需存储空间节省得多。
那么将得出如下结论:
寻找当前结点在中序下的后继:
当 rightchild == NULL,如果 rtag == 1,表示无后继
当 rightchild != NULL,如果 rtag == 0,后继为其右子树的中序遍历的第一个结点;如果 rtag==1 后继为右子女结点。
寻找当前结点在中序下的前驱:
当 leftchild == NULL,如果 ltag == 1,表示无前驱
当 leftchild != NULL,如果 ltag == 0,前驱为其左子树中序遍历的最后一个结点;如果 ltag==1 前驱为左子女结点。
添加了 ltag 和 rtag 标志后的结点结构如下图所示:

之前的二叉树线索化后将如下图:

线索二叉树的实现
线索二叉树的节点定义
//线索二叉树结点类型
template <typename T>
struct ThreadNode
{
T data; //结点数据
ThreadNode<T> *leftChild, *rightChild; //左孩子和右孩子
int ltag, rtag; //左右子树标志位
ThreadNode(const T item) : data(item), leftChild(NULL), rightChild(NULL), ltag(0), rtag(0) {
} //结点类的构造函数
};
中序线索二叉树的创建
对于一个已存在的二叉树按中序遍历进行线索化的算法中用到了一个指针 pre,它在遍历过程中总是指向遍历指针 p 的中序下的前驱结点,即在中序遍历过程中刚刚访问过的结点。在做中序遍历时,只要一遇到空指针域,立即填入前驱或后继线索。
//使用前序遍历创建二叉树(未线索化)
void CreateTree(ThreadNode<T> *&subTree)
{
T item;
if (cin >> item)
{
if (item != RefValue)
{
subTree = new ThreadNode<T>(item); //构造结点
if (subTree == NULL)
{
cout << "空间分配错误!" << endl;
exit(1);
}
CreateTree(subTree->leftChild); //递归创建左子树
CreateTree(subTree->rightChild); //递归创建右子树
}
else
{
subTree = NULL;
}
}
}
//中序遍历对二叉树进行线索化
void createInThread(ThreadNode<T> *current, ThreadNode<T> *&pre)
{
if (current == NULL)
{
return;
}
createInThread(current->leftChild, pre); //递归左子树的线索化
if (current->leftChild == NULL) //建立当前结点的前驱结点
{
current->leftChild = pre;
current->ltag = 1;
}
if (pre != NULL && pre->rightChild == NULL) //建立当前结点的后继结点
{
pre->rightChild = current;
pre->rtag = 1;
}
pre = current; //用前驱记住当前的结点
createInThread(current->rightChild, pre); //递归右子树的线索化
}
//中序遍历对创建好的普通二叉树进行中序线索化
void CreateInThread()
{
ThreadNode<T> *pre = NULL; //第一个结点的左子树置为NULL
if (root != NULL)
{
createInThread(root, pre);
//处理中序遍历的最后一个结点,最后一个结点的右子树置为空
pre->rightChild = NULL;
pre->rtag = 1;
}
}
中序线索化二叉树的成员函数
//寻找中序下第一个结点
ThreadNode<T> *First(ThreadNode<T> *current) //返回以*current为根的中序线索二叉树中序遍历的第一个结点
{
ThreadNode<T> *p = current;
while (p->ltag == 0)
{
p = p->leftChild; //循环找到最左下角结点
}
return p;
}
//寻找中序下的后继结点
ThreadNode<T> *Next(ThreadNode<T> *current)
{
ThreadNode<T> *p = current->rightChild;
if (current->rtag == 0)
{
return First(p)

本文介绍了线索二叉树的概念,如何通过在二叉树中增加线索指针来记录结点的前驱和后继,以减少遍历次数并优化查找效率。详细讲解了线索化的过程,以及如何通过标志位区分普通指针与线索,包括中序线索化二叉树的创建、成员函数和遍历算法实例。
最低0.47元/天 解锁文章
1482

被折叠的 条评论
为什么被折叠?



