二叉树遍历时从任一节点出发只能找到左右孩子,而一般的情况下无法直接找到该结点在某种遍历序列中的前驱和后继结点,这种信息只能在遍历的动态过程中才能得到。
但是,如果在首次遍历二叉树的过程中以某种方式保存了遍历序列中的前驱和后继结点的信息,则以后就可以根据该信息,加快相应的遍历过程。
考虑到一个具有n个结点的二叉树表,在2n个指针中只有n-1个是用来存储孩子结点的地址,存在n+1个空指针,可以利用这些空指针存放指向该结点在某种遍历序列中的前驱和后继结点的指针。这些指向前驱和后继结点的指针称为线索,加上线索的二叉树称为线索二叉树。
在线索链表中,对于任意结点,若左指针为空,则用左指针域存放该结点的前驱线索,若右指针域为空,则用右指针域存放该结点的后继线索。为了区分指针域存放的是指向孩子的指针还是指向前驱或者后继的线索,每个结点再增设两个标志域ltag和rtag。若标志为0则child指向结点的左孩子,若为1则指向结点的前驱或者后继。
可用C++中的结构类型描述线索链表中的结点:
enum flag {Child, Thread};
template<typename T>
struct TBNode
{
T data;
TBNode<T> * lchild, * rchild;
flag ltag, rtag;
};
如图是某树的中序线索二叉树示意图
结点D的左线索为空,表明它是中序遍历的开始结点,结点F的右线索为空,表明它是中序遍历的终止结点。
结点是叶子结点的充要条件是它的左右标志均是1。
注意:由于二叉树的遍历有四种,故有4中意义下的前驱和后继。也就是说有4种线索二叉树。下面的讨论以中序线索二叉树为主。
template<typename T>
class ThreadBinaryTree
{
public:
ThreadBinaryTree();
~ThreadBinaryTree();
TBNode<T> * Getroot();
TBNode<T> * Prior(TBNode<T> * p); //查找结点p的前驱
TBNode<T> * Next(TBNode<T> * p); //查找结点p的后继
void InOrder(TBNode<T> * root); //遍历线索二叉树
private:
TBNode<T> * root;
TBNode<T> * CreatBtree(); //构造函数调用,创建二叉树
void ThrBiTree(TBNode<T> * root, TBNode<T> * &pre); //线索化二叉树
};
建立中序线索二叉树
建立线索链表。实质上就是将二叉链表里的空指针改为指向前驱或者后继的线索,而这些线索必须在遍历的过程中才能得到,所以首先要建立二叉链表,然后在遍历的过程中修改指针。
在首次遍历的过程中,访问结点的操作是检查当前结点的左右指针域是否为空,如果为空,将他们改为指向前驱或后继的线索。为实现这一过程,设指针pre始终指向刚刚访问过的结点,即若指针指向当前的结点,则pre指向它的前驱,以便增设线索。
因此建立中序线索二叉树的递归算法跟中序遍历算法类似,只需要将中序遍历算法中访问结点的操作具体化为建立正在访问结点与其非空中序前驱结点间的线索。附设指针pre并始终保持指针pre指向当前访问的指针root所指向的前驱。
template <typename T>
ThreadBinaryTree<T>::ThreadBinaryTree() {
TBNode<T> * pre = NULL;
root = CreatBtree();
ThrBiTree(root, pre);
}
template<typename T>
TBNode<T> * ThreadBinaryTree<T>::CreatBtree() {
TBNode<T> *root;
T ch;
cin >> ch;
if (ch == '#') root = NULL;
else
{
root = new TBNode<T>;
root->data = ch;
root->ltag = Child;
root->rtag = Child;
root->lchild = CreatBtree();
root->rchild = CreatBtree();
}
return root;
}
template<typename T>
void ThreadBinaryTree<T>::ThrBiTree(TBNode<T> * root, TBNode<T> * pre) {
if (root == NULL) return; //递归结束条件
ThrBiTree(root->lchild, pre); //中序线索化左子树
if (!root->lchild) { //左孩子不存在,建立前驱线索
root->ltag = Thread;
root->lchild = pre; //设置当前结点pre的前驱线索
}
if ((pre != NULL) && (!pre->rchild)) { //设置pre的后继线索
pre->rchild = root;
pre->rtag = Thread;
}
pre = root; //令pre指向下一个访问结点的中序前驱
ThrBiTree(root->rchild, pre); //中序线索化右子树
}
获得指向根结点的指针
template<typename T>
TBNode<T> * ThreadBinaryTree<T>::Getroot() {
return root;
}
查找结点p的后继
在中序线索二叉树中,查找结点*p的中序后继结点分两种情形:
1、若*p的右子树为空,则p->rchild为右线索,直接指向*p的中序后继;
2、若*p的右子树非空,则*p的中序后继必定是其右子树中第一个中序遍历到的结点,也就是说,从*p的右孩子开始,沿该孩子的左链往下查找,直至找到一个没有左孩子的结点为止。该结点是*p右子树中最左下的结点。
template<typename T>
TBNode<T> * ThreadBinaryTree<T>::Next(TBNode<T> * p) {
TBNode<T> * q;
if (p->rtag == Thread) {
q = p->rchild;
} else {
q = p->rchild;
while (q->ltag == Child)
q = q->lchild;
}
return q;
}
查找结点p的前驱
思路跟查找结点p的后继类似,将”右子树的最左下结点”改成”左子树的最右下结点”即可.
template<typename T>
TBNode<T> * ThreadBinaryTree<T>::Prior(TBNode<T> * p) {
TBNode<T> * q;
if (p->ltag == Child) {
q = p->lchild;
} else {
q = p->lchild;
while (q->rtag == Child)
q = q->rchild;
}
return q;
}
遍历线索二叉树
遍历某种次序的线索二叉树只要从该次序下的开始结点出发,反复找到结点在该次序下的后继,直至终端结点。这对于中序和先序线索二叉树是十分简单的。在中序线索二叉树上的遍历算法如下:
template<typename T>
void ThreadBinaryTree<T>::InOrder(TBNode<T> * root) {
TBNode<T> * p = root;
if (root == NULL) return;
while (p->ltag == Child) //查找中序遍历序列的第一个结点并访问
p = p->lchild;
cout << p->data << ' ';
while (p->rchild != NULL) { //当p存在后继,依次访问其后继结点
p = Next(p);
cout << p->data << ' ';
}
cout << endl;
}
上述线索二叉树中,均建立了左右两个线索,但在许多实际应用中,往往只需要建立左右两个线索中的一种,并非一定需要全线索二叉树。
另外,可以仿照单链表的头结点,为线索二叉树也增加一个头结点,并使头结点的左指针指向根结点,右指针指向其遍历序列的开始结点和终端结点,则运算会更加方便。