C数据结构与算法-基础整理-树-06:线索二叉树(一)

线索二叉树的原理和实现代码

关于二叉树可以查看以下文章:

C数据结构与算法-基础整理-树-02:二叉树的建立及四种遍历方式

具体前序和后序方式线索化,可以参考以下文章:

C数据结构与算法-基础整理-树-07:线索二叉树(二)

0x01.线索二叉树由来

在普通二叉树中,叶结点或只有一个孩子的结点会存在许多空指针域,这些空间已经创建了,但并没有得到有效的利用,造成了空间的浪费

解决办法:

将左孩子的空指针指向父结点的前驱,并设置Ltag标志变量来指明,左孩子是指向前驱还是真正存在。

将右孩子的空指针指向父结点的后继,并设置Rtag标志变量来指明,右孩子是指向后继还是真实存在。

相关概念说明:

前驱:在以某种特定的遍历顺序下遍历二叉树时,一个结点上一个遍历到的结点。

后继:在以某种特定的遍历顺序下遍历二叉树时,一个结点下一个遍历到的结点。

线索:指向前驱和指向后继的指针叫线索。

线索二叉树:加上线索的二叉树。

线索化:对二叉树的空指针加上线索的过程叫线索化。

0x02.线索二叉树的结构

与普通二叉树不同的是,线索二叉树的结构中多了一个Ltag和Rtag的标志变量,来指明左孩子和右孩子的身份。

此外,线索二叉树线索化过程中,需要一个全局的pre指针,指向刚刚访问过的元素。

typedef struct TreeNode
{
	char data;
	struct TreeNode* Left;
	struct TreeNode* Right;
	int Ltag;//用于线索化的标志变量  左为1表示前驱 右为1表示后继,0表示自身是树中元素
	int Rtag;
}TreeNode,*BinTree;//定义了这个结构体为 TreeNode ,这个二叉树指针为 BinTree
BinTree pre;

0x03.线索二叉树的建立

线索二叉树的建立与普通二叉树的建立基本相同,不同的是,线索二叉树每增加一个结点,都需要把左右标志变量初始化。

具体建立代码和普通二叉树的建立相似,参考篇章:点这前往

说明:二叉树的何种建立方式与线索化,遍历均不冲突,仍和顺序建立的二叉树本质是一样的。

//二叉树的建立:递归实现,先序遍历的创建方法
//注意此处BT为指针的指针,因为没有返回值,所以要把二叉树的指针完全改变掉,需要指针的指针
//str为传入的字符串,该字符串用于创建二叉树的数据域
//n为当前创建节点数据域时使用到str的位数
//返回值为记录二叉树创建完毕后,使用了多少字符
int CreateTree(BinTree *BT,char *str,int n)
{
	printf("创建二叉树第  %d  个节点   ", n);
	char ch = str[n];//每次使用一个字符赋值数据域
	printf("%c\n", ch);//输出该数据
	n++;
	if (ch != '\0')//字符串没完就一直创建
	{
		if (ch == '#')//如果遇到字符 # ,说明下方没有了,是叶节点
		{
			*BT = NULL;//代表二叉树指针为空
		}
		else
		{
			*BT = (BinTree)malloc(sizeof(TreeNode));//*BT代表二叉树指针的值,BinTree表示这是一个指针,大小为一个二叉树结构体
			if (!*BT)
			{
				exit(-1);//若分配空间失败,异常退出
			}
			(*BT)->data = ch;
			(*BT)->Ltag = 0;//注意一定要有这个初始化步骤
			(*BT)->Rtag = 0;
			n=CreateTree(&(*BT)->Left,str, n);//取指针的地址给此函数的第一个参数,因为此参数为二级指针
			n=CreateTree(&(*BT)->Right, str, n);
		}
	}
	return n;
}

0x04.线索二叉树的中序线索化

线索化可以以多种顺序进行,但线索化顺序和遍历顺序只能一一对应。

//中序遍历线索化
void inThreading(BinTree BT)
{
	if(BT)
	{
		inThreading(BT->Left);
		if (!BT->Left)//若左孩子不存在,则左孩子线索化,指向前驱节点,即上一个访问过的节点
		{
			BT->Ltag = 1;
			BT->Left = pre;
		}
		if (pre && !pre->Right)//当遍历到BT时,若它的前驱pre存在,且前驱右孩子不存在,那么前驱的右孩子指向BT,即BT为pre的后继
		{
			pre->Rtag = 1;
			pre->Right = BT;
		}
		pre = BT;//当准备遍历下一个节点时,用pre保存当前的节点
		inThreading(BT->Right);//继续中序遍历完成线索化
	}
}

0x05.线索二叉树的中序遍历

与中序方式线索化二叉树相对应。

//中序遍历线索二叉树,非递归实现
void InOrderThreading(BinTree BT)
{
	BinTree p = BT;//让p指向根节点
	while (p)
	{
		while (p->Ltag == 0)
		{
			p = p->Left;
		}//首先遍历到中序遍历第一个开始的节点
		printf("%c -> ", p->data);//打印其数据
		while (p->Rtag == 1&&p->Right)//如果右孩子是线索,那么一直遍历至右孩子,因为右孩子指向后继
		{
			p = p->Right;
			printf("%c -> ", p->data);
		}//右孩子不是线索了,说明右孩子存在,左孩子刚遍历到了,所以接着右孩子的遍历,然后开始下一轮的遍历
		p = p->Right;
	}
}

0x06.加一个头结点的线索二叉树

在二叉树根结点前增加一个头结点,其左孩子指向根结点,右孩子指向中序遍历的最后一个结点,这样,二叉树就像一个双向链表,既可以从头遍历,也可以从尾遍历。

线索化:

//增加头结点的中序线索化,Head为二级指针,指向指针的指针,目的是真正改变一级指针的内容
void InOrderThreading(BinTree* Head, BinTree BT)
{
	*Head =(BinTree)malloc(sizeof(TreeNode));//给头结点分配空间
	(*Head)->Ltag = 0;//左孩子是根结点
	(*Head)->Rtag = 1;//右孩子是线索
	(*Head)->Right = (*Head);//右孩子先指向自身,防止原二叉树为空
	if (!BT)//二叉树为空
	{
		(*Head)->Left = (*Head);//左孩子也指向自身
		return;
	}
	pre = (*Head);//前指针指向头结点
	(*Head)->Left = BT;
	inThreading(BT);//开始普通的中序线索化二叉树
	//此时二叉树中序序列的第一个结点的左孩子指向头结点
	//结束普通的线索化后,此时的pre指针指向最后一个元素
	pre->Right = *Head;
	pre->Rtag = 1;
	(*Head)->Right = pre;
	//现在整个二叉树就是一个双向链表,既可以从头结点开始往后遍历,也可以从中序序列最后一个结点开始往前遍历
}

遍历:

//有头结点的中序遍历线索二叉树,此时的BT是头结点
void InOrderThreading1(BinTree BT)
{
	BinTree p = BT->Left;//让p指向根节点
	while (p!=BT)//循环到尾部时,因为尾部右孩子指向头结点,所以,p等于头结点的时候遍历完成
	{
		while (p->Ltag == 0)
		{
			p = p->Left;
		}//首先遍历到中序遍历第一个开始的节点
		printf("%c -> ", p->data);//打印其数据
		while (p->Rtag == 1 && p->Right!=BT)//此处右孩子的条件应为不等于BT
		{
			p = p->Right;
			printf("%c -> ", p->data);
		}//右孩子不是线索了,说明右孩子存在,左孩子刚遍历到了,所以接着右孩子的遍历,然后开始下一轮的遍历
		p = p->Right;
	}
}

带头结点的主函数测试代码:

int main()
{
	BinTree BT;//注意BT为指针
	int k;//记录创建函数的返回值
	char str[100];
	printf("\n\n**********\t请输入一串字符用于创建二叉树:");
	gets_s(str);
	k = CreateTree(&BT, str, 0);
	printf("\n\n**********\t二叉树创建成功!!!共使用了  %d  个字符",k);
	BinTree Head;
	printf("\n\n**********\t开始中序线索化该二叉树......");
	InOrderThreading(&Head,BT);
	printf("\n\n**********\t线索化成功!!!");
	printf("\n\n**********\t开始该线索二叉树的中序遍历......");
	printf("\n\n**********\t中序遍历结果为:");
	InOrderThreading1(Head);
	
}

测试数据:AB#D##C##

0x07.解惑与感悟

1.二叉树的空指针应该是有限的,为什么只在这些空指针上线索就能把整棵树线索化,以达到每个结点都能找到后继?

答:观察之前中序遍历的特点,从左孩子返回就打印结点信息,从右孩子返回就结束该步调用,注意打印结点的这块,才是真正的遍历到了该结点,那么是如何从左孩子返回的呢,当左孩子为空,或左孩子部分已经完成整个的函数调用过程,若左孩子为空,那么刚好,左孩子指向前驱,若左孩子部分已完成函数调用过程,那么左孩子是从左孩子的右孩子返回后完成整个调用的,依次类推,总会找到返回的根本:是从叶结点返回了,可以这样理解,凡是遍历到了左右孩子都存在的结点,那么它的上一个结点和下一个结点一定有空的部分,即左右孩子不全存在,这样,两个孩子都存在的结点就会有前驱和后继指针的存在了,再者,可以看一下它们之间的数量关系:对于一个有n个结点的二叉树,一共有2n个指针域,n个结点一共有n-1条分支,所以其中的2n-(n-1)=n+1个指针其实是空指针域,但其实n-1个指针就能完成树中关系的指向(即使某个结点有两个孩子,也能按照中序遍历的顺序往下寻找,寻找到前驱后继),其中第一个遍历到的结点无前驱,最后一个遍历到的结点无后继。所以,其实空指针域是足够的。

2.什么情况下用线索二叉树?

答:当经常需要遍历或查找结点时,可以使用,它的时间复杂度远低于递归的方法。

3.理解了中序遍历的线索化后,其实前序,后序,原理也差不多。

发布了19 篇原创文章 · 获赞 7 · 访问量 414

猜你喜欢

转载自blog.csdn.net/ATFWUS/article/details/104291897