数据结构——线索二叉树

——本节内容为Bilibili王道考研《数据结构》P46~P48视频内容笔记。


目录

一、线索二叉树的概念

1.引入

2.中序线索二叉树

3.中序线索二叉树的存储结构

 4.先序线索二叉树

二、二叉树的线索化

1.找中序序列前驱

2.中序线索化

3.先序线索化

4.后序线索化

三、线索二叉树找前驱、后继

1.中序线索二叉树找中序后继

2.中序线索二叉树找中序前驱

3.先序线索二叉树找先序后继

4.先序线索二叉树找先序前驱

5.后序线索二叉树找后序前驱

6.后序线索二叉树找后序后继


一、线索二叉树的概念

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没有后序后继。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值