线索二叉树
- 思考:在有n个节点的二叉链表中必定有n+1个空链域(下面会说为什么),遍历运算是最重要也是最常用的运算,之前的无论递归与非递归算法实现遍历效率都不算高。能不能利用这未使用的n+1空指针域,设计出提高遍历效率的存储结构?
观察该树可以发现节点(除了根节点)都有一个直接前驱,那么就会知道 如果节点数量为n 树枝数量为b,那么b=n-1;
链表每个节点都是有两个指针域的,所以n个节点有2n个,那么用掉n-1个就剩下上面说的n+1个空的了
那对于上面的思考:引入正题
线索二叉树就是利用那未使用的空指针域来优化,提高遍历效率的存储结构;
- 实现:在二叉链表的节点中增加两个标志域
当ltag或rtag为0时说明其有左/右孩子,该指针已经被占用,只有当ltag或rtag为1时才能使用
- ltag:若ltag = 0,left域指向左孩子 ; 若ltag = 1,left区域指向其遍历序列的前驱;
- rtag:若rtag = 0,left域指向右孩子 ; 若rtag = 1,right区域指向其遍历序列的后继;
我们只能使用未使用的指针,例如D的左指针和右指针,所以我们是用空指针域来指向其前驱和后继left ltag data rtag right
以中序线索二叉树为例(可以为前中后):
若中序序列为:D B G E A F H C K
解释:
其中虚线为线索指针,实线为指向真实子女
- 因为A的ltag和rtag标志位都为0,所以A有左右子节点,left和right指向它的真实子女;
- B节点和A节点类似;
- D节点的两个标志位都为1,所以其left和right要指向它的前驱和后继,但是D在中序序列中是第一个,没有前驱所以left为NULL,right指向B;
- E节点的ltag=0,所以指向子节点;rtag=1,所以指向后继
- 以此类推
如果我们已经构造二叉链表了,我们该怎样把它转化成线索二叉树呢?
- 我们来看一下线索二叉树的思想:
下面我们来实现一下线索二叉树
- 二叉树的存储结构:
/* 二叉树的二叉线索存储结构定义*/
typedef enum{Link, Thread}PointerTag; //Link = 0表示指向左右孩子指针;Thread = 1表示指向前驱或后继的线索
typedef struct BitNode
{
char data; //结点数据
struct BitNode *lchild, *rchild; //左右孩子指针
PointerTag Ltag; //左右标志
PointerTag rtal;
}BitNode, *BiTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继信息只有在遍历该二叉树时才能得到,所以,线索化的过程就是在遍历的过程中修改空指针的过程。
- 中序遍历线索化的递归函数代码如下:
BiTree pre; //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiTree p)
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
//===
if(!p->lchild) //没有左孩子
{
p->ltag = Thread; //前驱线索
p->lchild = pre; //左孩子指针指向前驱
}
if(!pre->rchild) //没有右孩子
{
pre->rtag = Thread; //后继线索
pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)
}
pre = p;
//===
InThreading(p->rchild); //递归右子树线索化
}
}
- 因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild做判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild = p,并且设置pre->rtag = Thread,完成后继结点的线索化
- if(!p->lchild)表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值了pre,所以可以将pre赋值给p->lchild,并修改p->ltag = Thread(也就是定义为1)以完成前驱结点的线索化。
-
完成前驱和后继的判断后,不要忘记当前结点p赋值给pre,以便于下一次使用
有了线索二叉树后,对它进行遍历时,其实就等于操作一个双向链表结构。
-
和双向链表结点一样,在二叉树链表上添加一个头结点,如下图所示,并令其lchild域的指针指向二叉树的根结点(图中第一步),其rchild域的指针指向中序遍历访问时的最后一个结点(图中第二步)。反之,令二叉树的中序序列中第一个结点中,lchild域指针和最后一个结点的rchild域指针均指向头结点(图中第三和第四步)。这样的好处是:我们既可以从第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
遍历代码如下:
//t指向头结点,头结点左链lchild指向根结点,头结点右链rchild指向中序遍历的最后一个结点。
//中序遍历二叉线索树表示二叉树t
int InOrderThraverse_Thr(BiTree t)
{
BiTree p;
p = t->lchild; //p指向根结点
while(p != t) //空树或遍历结束时p == t
{
while(p->ltag == Link) //当ltag = 0时循环到中序序列的第一个结点
{
p = p->lchild;
}
printf("%c ", p->data); //显示结点数据,可以更改为其他对结点的操作
while(p->rtag == Thread && p->rchild != t)
{
p = p->rchild;
printf("%c ", p->data);
}
p = p->rchild; //p进入其右子树
}
return OK;
}
对于上述代码:
- 代码中,p = t->lchild;意思就是上图中的第一步,让p指向根结点开始遍历;
- while(p != t)其实意思就是循环直到图中的第四步出现,此时意味着p指向了头结点,于是与t相等(t是指向头结点的指针),结束循环,否则一直循环下去进行遍历操作;
- while(p-ltag == Link)这个循环,就是由A->B->D->G,此时H结点的ltag不是link(就是不等于0),所以结束此循环;
- 然后就是打印H;
- while(p->rtag == Thread && p->rchild != t),由于结点B的rtag = Thread(就是等于1),且不是指向头结点。因此打印D的后继B,之后因为B的rtag是Link,因此退出循环;B
- p=p->rchild;意味着p指向了结点B的右孩子了;
- 就这样不断的循环遍历,直到打印出D B G E A F H C K,结束遍历操作。
从这段代码可以看出,它等于是一个链表的扫描,所以时间复杂度为O(n)。
由于充分利用了空指针域的空间(等于节省了空间),又保证了创建时的一次遍历就可以终生受用后继的信息(意味着节省了时间)。所以在实际问题中,如果所用的二叉树需要经过遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。
哈夫曼树:https://blog.csdn.net/alzzw/article/details/97809047
二叉搜索(排序)树;https://blog.csdn.net/alzzw/article/details/97563011
平衡二叉树:https://blog.csdn.net/alzzw/article/details/97613193