二叉树的 遍历方式有4种,前序线索二叉树,中序线索二叉树,后序线索二叉树,层序线索二叉树。此处使用中序线索二叉树举例,其他三种建立方式类似,只是代码位置不同。
线索化的意义: 如果一个二叉树通过中序遍历结果为 a d f g b c;这是通过递归调用得到的遍历结果,线索化就是再遍历一遍,将所有结点连接起来,构成一个 链表,之后就可以通过遍历链表进行遍历访问,不必使用递归遍历.
重点1: 线索二叉树传递进来的指针要以引用的方式,否则改变指针的指向,改变的只是临时指针的指向。(凡是要更改指针指向的都传递指针的引用,不能是传递指针作为参数)
重点2:不论是前, 中, 后序线索化二叉树, 都需要对最后一个结点单独处理,处理最后结点, 是在新的函数中单独处理,不能在线索化函数中处理,因为线索化是调用自身递归,当利用循环线索化结束时候,pre和p同时指向了最后一个结点,此时单独处理最后一个结点 也就是pre->rchild=NULL; pre->rtag = 1;
重点3: 前,中,后序线索化的二叉树不一样。(前序是根据前序遍历得到的结果,将结果连接起来,中序线索化是将中序遍历得到的结果,将结果用指针连起来,所以前,中,后线索化的结果不同)
前 中 后序线索化的区别:
1. 前序线索化需要使用 ltag进行判断,因为p->lchild=NULL时, 让p->lchild=NULL; 根据前序遍历规则,p结点访问结束,现在需要对p->lchild孩子进行递归,但是此时p->lchild已经指向了前一个结点,就会导致在前一个结点和这个结点往复循环,导致不能线索化后面的结点
2.中序和后序线索化不需要使用ltag和rtag进行判断,因为根据中序和后序规则, 当访问p结点时候,它的左孩子(或者说前驱已经访问过了),不会造成往复循环,前序是可以指向前驱,再递归进来访问的是前驱的孩子,再指向前驱,再递归前驱的孩子,一直往复循环.
//创建线索二叉树结点结构
class node{
public:
char data;
node *lchild,*rchild;
int ltag,rtag;
};
当ltag=0和rtag=0的时候,表示*lchild和*rchild含义不变,任然指向他们的左孩子和右孩子;
当ltag=1和rtag=1时候,表示*lchild和*rchild不再指向他们的左右孩子了,
而是lchild指向结点的前驱,rchild指向结点的后继
现在有如下图的二叉树,中序序列: D G B A E C F
经过线索二叉树后,所有结点,如果左孩子为空,就让左孩子指向前驱,如果右孩子为空,
就让右孩子指向后继.
中序序列: D G B A E C F //可以看出G的左孩子指向D,G的右孩子指向B
当最后一个结点F->rchild应该指向NULL,但右孩子是用pre来控制的,pre无法到达最后的结点F,当最后一次循环,利用循环线索化结束后,需要手动对最后一个结点进行处理,即此时pre和p都指向了最后一个结点,需要手动设置pre->rchild = NULL, 同时手动设置pre->rtag=1
先创建一个二叉树,并且把所有的ltag和rtag设置成0;
创建二叉树的方式(可以看下面链接这篇文章):
创建完二叉树后,开始遍历二叉树,在遍历的过程中,将二叉树线索化。
从开始遍历建立线索二叉树只做下面几件事.
p是当前结点,pre是p的前一个结点(pre是一个全局变量).
1.判断p的左孩子是否为空,若为空,设置前驱线索,执行下面代码,如果不为空,不做任何操作.
1.1 p->ltag=1; //设置左线索为1
1.2 p->lchild = pre; //左孩子为空,就让左孩子指向前一个结点
2.判断pre是否为空,如果为空,则设置后继.执行下面代码,如果不为空,不做任何操作.
2.1 pre->rtag=1; //设置右线索为1
2.2 pre->rchild = p; //右孩子为空,则让右孩子指向下一个结点
3.移动pre结点,并且让p移动到新的结点
pre = p; //让pre指向p
(pre指针有三种定义方式,1.将pre定义成全局变量,2.将pre定义成属性 3.将pre定义成静态变量)
inThread(p->rchild) ///右子树线索化
整体代码
//定义线索二叉树结构
class ThreadBitree
{
public:
node *root; //定义一个根节点
node *pre; //定义pre指针用于记录前一个结点的位置
ThreadBitree()
{
pre = NULL;
creat(root) //因为是引用的方式,所以可以直接操作根节点root
inThread(root); //调用线索化函数将二叉树线索化
}
//中序遍历线索化二叉树
//实际就是一个中序遍历二叉树,只是把访问结点改了.
inThread(node* &p) //利用引用可以修改指针的内容
{
if(p==NULL) //如果结点为空,则返回. 否则根据中序,进入左子树
return;
inThread(p->lchild); //递归进入左子树将左子树线索化 当为空的时候返回来,执行下面代码
//ltag=0表示有左孩子, ltag=1,表示有前驱
if(p->lchild == NULL)
{
P->ltag=1;
p->lchild = pre;
}
if(pre != NULL && pre->rchild == NULL)
{
pre->rtag = 1;
pre->rchild = p;
}
//更新pre和p的值,让pre=p,p->rchild作为参数在赋值给右子树,
//当进入新一层函数时候,pre记录的是上一层的地址,p记录的是本次的地址,满足线索树的需求
pre = p; //先让pre记录当前结点,再让p进入下一结点。
inThread(p->rchild) //右子树线索化
}
void creat(node* &bt); //用于创建二叉树,这里需要ltag和rtag设置成0
前序创建线索二叉树,
如图:
前序线索化递归的时候需要用两个if语句对ltag和rtag进行判断,比如: if(ltag==0)表明它有左子树,则将左子树传入递归,如果不进行判断,此时左孩子已经指向了前驱,再将左孩子传进去递归,则会一直在这两个结点间循环递归,跳不出去循环,如果使用if(ltag==0) 表明是它自己的左孩子,那么就递归它的左孩子,如果ltag==1,表示是它的前驱,则不进行递归。如果左右孩子都为NULL,那么就返回到上一层调用的位置继续执行调用位置下面的代码.
inThread(node* &p) //使用引用可以修改指针的内容
{
if(p==NULL) //如果结点为空,则返回. 否则根据中序,进入左子树
return;
if(p->lchild == NULL)
{
P->ltag=1;
p->lchild = pre;
}
if(pre != NULL && pre->rchild == NULL)
{
pre->rtag = 1;
pre->rchild = p;
}
//更新pre和p的值,让pre=p,p->rchild作为参数在赋值给右子树,
//当进入新一层函数时候,pre记录的是上一层的地址,p记录的是本次的地址,满足线索树的需求
pre = p; //先让pre记录当前结点,再让p进入下一结点。
//先序需要看ltag和rtag,当ltag或rtag=1时候,说明该结点被线索化了,就不需要再线索化
if(p->ltag == 0){
inThread(p->lchild);
}
if(p->rtag==0){
inThread(p->rchild) //右子树线索化
}