——本节内容为Bilibili王道考研《数据结构》P46~P48视频内容笔记。
目录
一、线索二叉树的概念
1.引入
(1)普通的二叉树,我们之前学到可以写出其中序遍历序列。这个序列中的结点可以找到它们的前驱或后继结点,那我们能不能直接找到某个指定结点的前驱和后继结点(中序遍历序列中的前驱后继)呢?或者说我们可不可以从一个指定的结点开始中序遍历呢?
(2)这里给出解决思路:
①定义指针p指向这个指定结点;
②从根结点出发,重新进行中序遍历,同时定义指针p来记录当前所访问的结点,定义指针pre来记录上一个被访问的结点;
③当q==p时,此时pre即指向了指定结点p的前驱;
④当pre==p时,此时q即指向了指定结点p的后继;
(3)这个方法可行,但很麻烦,也就是每一次我们都要重新进行一次中序遍历操作,故引出概念“线索二叉树”。
2.中序线索二叉树
(1)前面提到:n个结点的二叉树,有n+1个空链域。我们可以利用这些空链域来记录前驱和后继的信息。
(2)例如下面这棵二叉树:
①这棵二叉树的中序遍历序列为:D G B E A F C;
②结点D为中序序列第一位,且其左子树结点为空,我们可以将这个空链域指向NULL,即表示D在中序序列中没有前驱;
③结点G为中序序列第二位,且其左右子树结点都为空,我们可以用G的左子树结点空链域指向G所在中序序列的前驱D,用右子树结点空链域指向G所在中序序列的后继B;
④按照③的操作,将E的左子树结点空链域指向B,右子树结点空链域指向A;
⑤将F的左子树结点空链域指向A,右子树结点空链域指向C;
⑥将C的右子树结点空链域指向NULL;
(3)线索化完成后如下图:
(4)图示中:
①黄色箭头为前驱线索(由左孩子指针充当);
②紫色箭头为后继线索(由右孩子指针充当);
③指向前驱、后继的指针称为“线索”;
3.中序线索二叉树的存储结构
(1)线索二叉树中,有的结点的左右孩子指针指向的是真正的自己的左右孩子,而有的结点的左右孩子指针指向的是前驱后继线索;怎样来区分呢?给出下面这种存储结构:
(2)代码实现:
typedef struct ThreadNode { //线索链表
int data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag; //左右线索标志
}ThreadNode,*ThreadTree;
(3)观察上面代码,我们在普通二叉树结构体的基础上又定义了int型ltag、rtag分别来代表左右线索标志:
①当tag==0时,表示指针指向孩子;
②当tag==1时,表示指针是“线索”;
(4)我们称这种链表为线索链表,直观表示如下:
4.先序线索二叉树
(1)同理根据先序遍历序列我们可以得到先序线索二叉树,如下:
(2)后序线索二叉树:
二、二叉树的线索化
1.找中序序列前驱
(1)在之前提到过一个找中序序列前驱结点的土办法,也就是重新进行一次中序遍历,定义三个指针:p指向指定结点,q指向当前访问结点,pre指向q的上一个访问的结点,最终当q==p时即找到了pre就是指定结点p的中序前驱。
(2)代码实现:
BiTNode* p;
BiTNode* pre = NULL;
BiTNode* final = NULL;
void visit(BiTNode* q)
{
if (q == p)
final = pre;
else
pre = q;
}
void FindPre(BiTree T)
{
if (T != NULL)
{
FindPre(T->lchild);
visit(T);
FindPre(T->rchild);
}
}
(3)代码解释:
①定义辅助全局变量:*p、*pre、*final来查找结点p的前驱;
②visit函数:如果q==p则pre就是我们要找的最终的前驱结点;不然将pre指向当前结点。
2.中序线索化
(1)代码实现:
typedef struct ThreadNode //线索二叉树结点
{
int data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag; //左右线索标志,初始值为0,也就是我们的所有结点都没有被线索化
}ThreadNode,*ThreadTree;
ThreadNode* pre = NULL;
void InThread(ThreadTree T) //中序遍历二叉树,一边遍历一遍线索化
{
if (T != NULL)
{
InThread(T->lchild); //中序遍历左子树
visit(T); //访问根结点
InThread(T->rchild); //中序遍历右子树
}
}
void visit(ThreadNode* q)
{
if (q->lchild == NULL) //左子树为空,建立前驱线索
{
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL) //建立前驱结点的后继线索
{
pre->rchild = q;
pre->rtag = 1; //最后一个结点的处理见CreateInThread
}
pre = q;
}
void CreateInThread(ThreadTree T) //中序线索化二叉树T
{
pre = NULL; //pre初始为NULL
if (T != NULL) //非空二叉树才能线索化
{
InThread(T); //中序线索化二叉树
if (pre->rchild == NULL)
pre->rtag = 1; //处理遍历的最后一个结点
}
}
(2)代码解释:
①线索二叉树结构体里再定义上ltag和rtag的左右线索标志,初始化为0,代表所有结点都没有被线索化;
②全局变量pre指向当前访问结点的前驱,初始化为NULL;
③InThread函数其实就是中序遍历函数;
④visit函数是对当前访问结点q的处理:左子树为空时,将左孩子指针指向q的前驱pre,并且将ltag=1表明该结点已被线索化;右子树为空时,将右孩子指针指向pre的后继q,并且将rtag=1,表明该结点已被线索化;最后将pre后移指向当前访问结点;
⑤CreateInThread函数指中序线索化函数,调用InThread函数来对一棵二叉树T进行中序线索化,树非空时开始进行:调用InThread函数对各个结点进行线索化,调用完后还需在本函数中对最后一个结点进行线索化。
(3)第二种实现方式:将pre作为形参放入函数中,代码如下:
void InThread(ThreadTree p, ThreadTree& pre)
{
if (p != NULL)
{
InThread(p->lchild, pre); //递归,线索化左子树
if (p->lchild == NULL) //左子树为空,建立前驱线索
{
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL)
{
pre->rchild = p; //建立前驱结点的后继线索
pre->rtag = 1;
}
pre = p;
InThread(p->rchild, pre);
}
}
//中序线索化二叉树T
void CreateInThread(ThreadTree T)
{
ThreadTree pre = NULL; //注意这个pre是局部变量,在传参的时候用了&
if (T != NULL) //非空二叉树,线索化
{
InThread(T, pre); //线索化二叉树
pre->rchild = NULL; //处理遍历的最后一个结点
pre->rtag = 1;
}
}
3.先序线索化
(1)代码同中序线索化相似:
要注意的是:在PreThread函数中,先序到左子树结点时,要判断一下左线索ltag是否是0,因为先序线索化可能出现q后移到已经线索化过的左子树结点,进入死循环。
typedef struct ThreadNode
{
int data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag;
}ThreadNode, * ThreadTree;
ThreadNode* pre = NULL;
void visit(ThreadNode* q)
{
if (q->lchild == NULL)
{
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL)
{
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
void PreThread(ThreadTree T)
{
if (T != NULL)
{
visit(T);
if (T->ltag == 0)
PreThread(T->lchild);
PreThread(T->rchild);
}
}
void CreatePreThread(ThreadTree T)
{
pre = NULL;
if (T != NULL)
{
PreThread(T);
if (pre->rchild == NULL)
pre->rtag = 1;
}
}
(2)pre作为形参的代码实现:
typedef struct ThreadNode
{
int data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag;
}ThreadNode, * ThreadTree;
void PreThread(ThreadTree p, ThreadTree &pre)
{
if (p != NULL)
{
if (p->lchild == NULL)
{
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL)
{
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
}
if (p->ltag == 0)
PreThread(p->lchild, pre);
PreThread(p->rchild, pre);
}
void CreatePreThread(ThreadTree T)
{
ThreadTree pre = NULL;
if (T != NULL)
{
PreThread(T, pre);
if (pre->rchild == NULL)
pre->rtag = 1;
}
}
4.后序线索化
(1)同理得代码:
typedef struct ThreadNode
{
int data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag;
}ThreadNode, * ThreadTree;
ThreadNode* pre = NULL;
void visit(ThreadNode* q)
{
if (q->lchild == NULL)
{
q->lchild = pre;
q->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL)
{
pre->rchild = q;
pre->rtag = 1;
}
pre = q;
}
void PostThread(ThreadTree T)
{
if (T != NULL)
{
PostThread(T->lchild);
PostThread(T->rchild);
visit(T);
}
}
void CreatePostThread(ThreadTree T)
{
pre = NULL;
if (T != NULL)
{
PostThread(T);
if (pre->rchild == NULL)
pre->rtag = 1;
}
}
(2)pre作为形参的代码:
typedef struct ThreadNode
{
int data;
struct ThreadNode* lchild, * rchild;
int ltag, rtag;
}ThreadNode, * ThreadTree;
void PostThread(ThreadTree p, ThreadTree& pre)
{
if (p != NULL)
{
PostThread(p->lchild, pre);
PostThread(p->rchild, pre);
if (p->lchild == NULL)
{
p->lchild = pre;
p->ltag = 1;
}
if (pre != NULL && pre->rchild == NULL)
{
pre->rchild = p;
pre->rtag = 1;
}
pre = p;
}
}
void CreatePostThread(ThreadTree T)
{
ThreadTree pre = NULL;
if (T != NULL)
{
PostThread(T, pre);
if (pre->rchild == NULL)
pre->rtag = 1;
}
}
三、线索二叉树找前驱、后继
1.中序线索二叉树找中序后继
(1)方法思路:
①若p.rtag==1,即p的右孩子指针本身就是指向p的中序后继,则next=p.rchild;
②若p.rtag==0,即右孩子指针没有被线索化,也就意味着p一定有右孩子。按照中序遍历的规则(左、根、右),如果p的右孩子结点为叶子结点,那么右孩子结点就是后继;如果p的右孩子结点还有分支,其分支还要按照左、根、右的顺序去访问,假设分支很多很多,每次都要先访问每个结点的左孩子结点,那么我们可以得到,最后一层的左孩子结点就是我们要找的后继,即next=p的右子树中最左下结点。
(2)代码实现:
//找到以p为根的子树中,第一个被中序遍历的结点
ThreadNode* Firstnode(ThreadNode* p)
{
while (p->ltag == 0) //循环找到最左下结点(不一定是叶结点)
p = p->lchild;
return p;
}
//在中序线索二叉树中找到结点p的后继结点
ThreadNode* Nextnode(ThreadNode* p)
{
if (p->rtag == 0)
return Firstnode(p->rchild); //右子树中最左下结点
else
return p->rchild; //rtag==1直接返回后继线索
}
//对中序线索二叉树进行中序遍历(利用线索实现的非递归算法)
void Inorder(ThreadNode* T)
{
for (ThreadNode* p = Firstnode(T); p != NULL; p = Nextnode(p))
visit(p);
}
2.中序线索二叉树找中序前驱
(1)方法思路:
①若p.ltag==1,即p的左孩子指针本身就是指向p的中序前驱,则next=p.lchild;
②若p.ltag==0,即左孩子指针没有被线索化,也就意味着p一定有左孩子。按照中序遍历的规则(左、根、右),如果p的左孩子结点为叶子结点,那么左孩子结点就是前驱;如果p的左孩子结点还有分支,其分支还要按照左、根、右的顺序去访问,假设分支很多很多,每次根结点前一个(前驱)都是下一层的右孩子指针,那么我们可以得到,最后一层的右孩子结点就是我们要找的前驱,即next=p的左子树中最右下结点。
(2)代码实现:
//找到以p为根的子树中,最后一个被中序遍历的结点
ThreadNode* Lastnode(ThreadNode* p)
{
while (p->rtag == 0) //循环找到最右下结点(不一定是叶结点)
p = p->rchild;
return p;
}
//在中序线索二叉树中找到结点p的前驱结点
ThreadNode* Prenode(ThreadNode* p)
{
if (p->ltag == 0)
return Lastnode(p->lchild); //左子树中最右下结点
else
return p->lchild; //ltag==1直接返回前驱线索
}
//对中序线索二叉树进行逆向中序遍历
void RevInorder(ThreadNode* T)
{
for (ThreadNode* p = Lastnode(T); p != NULL; p = Prenode(p))
visit(p);
}
3.先序线索二叉树找先序后继
(1)方法思路:
①若p.rtag==1,即p的右孩子指针本身就指向p的先序后继,则next=p.rchild;
②若p.rtag==0,即右孩子指针没有被线索化,也就意味着p一定有右孩子。按照先序遍历的规则(根、左、右),假设p有左孩子,则先序后继为p的左孩子,即next=p.lchild;假设p没有左孩子,则先序后继为p的右孩子,即next=p.rchild。
4.先序线索二叉树找先序前驱
(1)方法思路:
①若p.ltag==1,即p的左孩子指针本身就指向p的先序前驱,则next=p.lchild;
②若p.ltag==0,基于我们改用三叉链表能找到p的父结点的前提下,分为下面四种情况:
Ⅰ如果p是左孩子,则p的父结点为其前驱;
Ⅱ如果p是右孩子,且其左兄弟为空,则p的父结点为其前驱;
Ⅲ如果p是右孩子,且其左兄弟非空,则p的前驱为左兄弟子树中最后一个被先序遍历的结点;
Ⅳ如果p是根结点,则p没有先序前驱;
5.后序线索二叉树找后序前驱
(1)方法思路:
①若p.ltag==1,则pre=p.lchild;
②若p.ltag==0,即左孩子指针没有被线索化,也就意味着p一定有左孩子。按照后序遍历的规则(左、右、根),假设p有右孩子,则后续前驱为右孩子,即pre=p.rchild;假设p没有右孩子,则后续前驱为左孩子,即pre=p.lchild。
6.后序线索二叉树找后序后继
(1)方法思路:
①若p.rtag==1,则next=p.rchild;
②若p.rtag==0,基于我们改用三叉链表能找到p的父结点的前提下,分为下面四种情况:
Ⅰ如果p是右孩子,则p的父结点为其后继;
Ⅱ如果p是左孩子,且其右兄弟为空,则p的父结点为其后继;
Ⅲ如果p是左孩子,且其右兄弟非空,则p的后继为右兄弟子树中第一个被后序遍历的结点;
Ⅳ如果p是根结点,则p没有后序后继。