线索二叉树
二叉树常用二叉链表的形式存储,链表中的每个结点形式如下:
二叉链表存储的示意图如下,可以发现在二叉链表中如果一个结点没有左或右孩子结点,就会出现空指针域(^)的问题,如下:
当我们中序遍历二叉树时,得到的结果为:H D I B J E A F C G (粗体是包含空指针域的结点)
通过中序遍历的结果,可以发现每个同时有左右孩子的结点的边上都有一个存在空指针域的结点,通过利用这个空指针域来指向中序遍历的前驱后续来实现“双向”访问,这就是线索二叉树。
基本实现思想
通过上述的线索二叉树定义来看,如果结点的左孩子为空指针域时,让其中序遍历结果的前一个结点,这里称为前驱结点;同样在结点的右孩子为空指针域时,将其指向中序遍历结果的当前结点的下个结点,这里称为后继结点;
在决定lchild是指向左孩子还是前驱,rchild是指向右孩子还是后继,需要一个区分标志的,用来区分当前指向的结点代表的含义,因此,我们在每个结点再增设两个标志域ltag和rtag,这里的ltag和rtag只有0或1。
在结点结构如下所示:
表示:
(1)ptr->lchild非空,ltag为0, 此时指向该结点的左孩子;
ptr->lchild为空,ltag为1,此时指向前驱结点
(2)ptr->rchild非空,ltag为0, 此时指向该结点的右孩子;
ptr->rchild为空,ltag为1,此时指向后驱结点
a. 利用中序遍历中的节点(H D I B J E A F C G)空指针域来表示后续的示意图如下:
b. 利用中序遍历的节点(H D I B J E A F C G)空指针域来表示前驱的示意图如下:
c. 由于前驱和后继信息都是在中序遍历的基础上得到的;其实构建线索二叉树的过程就是在中序遍历的过程中修改空指针的过程,下面通过中序编译来完成线索化:
#include <iostream>
using namespace std;typedef enum { Link, Thread }PointerTag;
typedef struct BitNode
{
char data;
struct BitNode *lChild, *rChild;
PointerTag lTag;
PointerTag rTag;
}BitNode, *BiTree;//中序遍历的线索化过程
BiTree pre;
void InThreading(BiTree biTree)
{
if (biTree)
{
InThreading(biTree->lChild);
if (!biTree->lChild)
{
biTree->lTag = Thread;
biTree->lChild = pre;//前驱
}/*
*由于我们并不知道当前节点的后续节点是什么
*但是可以判断对前一个节点判断右孩子
*/
if (!pre->rChild)
{
pre->rTag = Thread;
pre->rChild = biTree;
}InThreading(biTree->rChild);
}
}
d. 上面我们已经知道了前驱、后续 以及如何对一个二叉树进行线索化,那么实现的线索化二叉树到底有什么意义?不妨通过遍历一个线索化的二叉树试试看:
和双向链表一样,在线索化的二叉树基础上添加一个头结点header,让header的lChild指向二叉树的root节点,rChild指向中序遍历的最后一个节点(即H D I B J E A F C G 顺序中的G节点),这里的biTree是header节点的指针。
void InOrderThraverse_Thr(BiTree biTree)
{
BiTree p = biTree->lChild;
while (p != biTree)
{
while (p->lTag == Link)
{
p = p->lChild;
}cout << "data: " << p->data << endl;
while (p->rTag == Thread && p->rChild != biTree)
{
p = p->rChild;
cout << "data: " << p->data << endl;
}p = p->rChild;
}
}
结合上面的图片,来看下遍历的执行过程:
1. BiTree p = biTree->lChild; 使 p 指向root节点;
2. 在循环 while (p != biTree) 中, 首先判断是否指向header节点,如果不是,进入循环内部;
3. 在循环while (p->lTag == Link)中首先判断当前结点的lChild是左孩子还是前驱,如果是左孩子直接循环找到第一个包含lChild为前驱的结点,则输出该结点的值,结合上图,输出的为 H
4. 在while (p->rTag == Thread && p->rChild != biTree)循环中,判断当前的节点的rChild是何种类型(右孩子还是后续节点),如何是后续结点并且不为头部节点,那么将当前 p 设为 rChild 指向的节点,并输出,结合上图这里指向 结点D,并输出 D
5.上述循环结束之后,将p 设置为结点D的rChild 指向的结点 I;
6. 紧接着来到while (p != biTree) 的第二次循环,这次p 指向的5中分析的 结点 I,因为结点I 包含了前驱和后续结点,所以进入循环内部 直接输出了 I,并将 p 指向到结点I的rChild所指向的结点 B,并输出 B;在循环内部再将 p 设置为结点 B 的 rChild,即 p 指向了 E ;
7. 紧接着来到while (p != biTree) 的第三次,第四次 。。。多次循环,按顺序输出 J E A F C G;
8. 综合以上的输出结果为 :H D I B J E A F C G
通过以上的分析,是不是很神奇,他竟然像双向链表一样,输出了中序遍历的结果,它等于是一个链表的扫描,所以时间复杂度为O(n)。
线索二叉树由于充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就可以终生受用后继的信息(意味着节省了时间)。所以在实际问题中,如果所用的二叉树需要经过遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
以上参考: