数据结构——二叉树线索化

什么是线索化

线索化的步骤:

  1. 根据某种遍历序列(前、中后序遍历),先确定下来每个节点的前驱和后继。
  2. 对于每个节点来说,他的左右指针可能没有指向节点(值为NULL),这时候我们可以运用这些“空闲”的指针。比如:左指针如果有空闲,就用这个指针指向这个节点对应遍历序列的前驱,右指针如果有空闲,就用这个指针指向这个节点对应遍历序列的后继。(注意:遍历序列中一头一尾是没有前驱或者后继的,所以如果指针有空闲,我们还是当它指向的是孩子,而不是前驱或者后继)
  3. 对于每个节点都实现了步骤2后,线索化完成

为什么要线索化

我们来线索化主要有两个原因

  1. 从空间上来说
    对于一颗有n个节点二叉树,每个节点都有两个指针,这一棵树的所有节点总共有2n个指针。对于除了根节点以外的节点,每个节点都对应着一个指向其的指针,有且仅有这些指针是非空的,共有(n-1)个指针,那么空值指针就有n+1个,这个数量是很大的,对于空间的浪费也比较多
  2. 从遍历实现上
    在我们用二叉树的递归遍历时,会遇到两个问题,一是:遍历需要的时间较多;二是:递归时候不断创建函数副本,对于内存来说也有一定压力。如果我们只用先进行一次递归遍历实现线索化,之后通过线索来遍历,能大大减少遍历时间和内存风险。

或者栈等数据结构来保持遍历的状态。而线索化后的二叉树可以通过线索(即额外的指针)直接找到前驱和后继节点,从而无需用额外的空间。这样可以提高遍历的效率和性能。

如何通过线索实现更简便的遍历

当想到通过线索来进行遍历的时候,我们也许会第一时间想到那个指向后继节点的右指针。

但是注意:线索化不是万能的,并不能帮助我们找到每个节点的前驱与后继,因为左右指针可能指向了孩子,这时候找不到前驱后继节点了。

但是,我们任然可以通过这些断断续续的线索关系实现简便的二叉树遍历,遍历的实现依赖于先、中、后序各自遍历的特点

先序遍历

先序的特点就是“根左右”,那么,访问的第一个节点一定是根节点,而且“根”访问到了,会继续遍历左子树,而左子树的第一个被访问的节点又会是左子树的根

也就是说,根节点的左孩子如果存在,那么我们一定是在根节点之后去访问左孩子。若是左孩子不存在,根节点的右孩子作为右子树的第一个节点,就会接着根节点被访问,这样从一棵树到它的左右子树,再到左右子树的左右子树,终究会访问到叶子结点,这时候,我们的叶子节点的右指针一定是指向后继,去访问这个后继节点。

总结一下,先序遍历在访问完一个节点会遇到三种情况:

  1. 左孩子存在,不管右孩子是否存在,访问左孩子
  2. 左孩子不存在但是右孩子存在,访问右孩子
  3. 左右孩子都不存在,访问后继节点

中序遍历

中序遍历的特点是“左根右”,回想起非递归中序遍历那里的思路,中序遍历开始时“一路向左”找到遍历的第一个节点。这个节点成为“根”了,根的后面接着就是“右”,接下来访问的就是这个节点的右子树了(如果右子树存在的话)。对于这个右子树,我们能做的还是一路向左,找到这个右子树的第一个节点。

这样一直找,终会访问到一个节点它没有右子树,这也意味着它没右孩子,这样我们又可以直接去找它的后继节点了

总结一下,中序遍历在访问完一个节点会遇到两种情况:

  1. 有右孩子,直接对右子树“一路向左”
  2. 没右孩子,通过右指针找后继节点

后序遍历

后序遍历的特点是“左右根”,那么对于当前访问到的一个节点来说,我们如果把它当作“根”来看待显然不合适,因为根的后面没有东西了,而且对于每一棵子树,根节点永远会是最后一个被访问到的节点。

那么我们把当前节点当作“右”看待,它就是右子树的根节点,访问完右子树的根节点意味着右子树访问完成,意味着接下来是“左右根”的“根”,也就是右子树根节点的父节点。也就是说,当前节点如果是它父节点的右孩子节点,接下来访问当前节点的父节点

按照上面的思路,或者当前节点是其父节点的左孩子节点而且父节点无右子树,下一个访问节点为父节点

那么在当前访问节点的的父亲的左右孩子都存在,并且当前节点为父节点的左孩子时,根据“根左右”,接下来就是去访问父节点的右子树中的第一个被访问到的节点(找到该节点可参考非递归后序遍历的第一个思路)

可以看到,后序遍历可以不依赖于线索化,但是它与前两个最大的不同在于:他需要去关注访问节点的父亲节点,也就意味着我们应该在每个节点中增加一个父亲域,指向当前节点的父亲。

总结一下,后序遍历在访问完一个节点会遇到三种情况:

  1. 当前节点为根节点,无后继(遍历结束,因为根节点是最后一个被访问的)
  2. 当前节点为右孩子or当前节点为左孩子且无右兄弟,访问父亲
  3. 当前节点为左孩子且有右兄弟,访问以右兄弟为根的子树的第一个被访问到的节点

线索化的代码实现

线索化后,每个节点的做左右指针指向的究竟是自己的孩子还是线索我们无法加以区分,这样,线索化后也会破坏原本二叉树的结构

因此,在对于二叉树节点的定义的是时候,我们设置两个标记变量,用来标记左右指针指向的时孩子还是线索

typedef struct BTNode { //二叉树结点     
	char data;//每个节点中存放的数据
	struct BTNode* left;
	struct BTNode* right;
	int ltag, rtag; //值为0表示指针指向孩子节点,值为1表示指向线索化的节点
}BTNode;

初始化节点的函数也要随之改变

//二叉树的节点创建函数
BTNode* CreateBTNode(char e) //传入一个数据,创建相应的节点
{
	BTNode* bn = (BTNode*)malloc(sizeof(BTNode));
	bn->data = e;
	bn->right = bn->left = NULL;
	bn->ltag = bn->rtag = 0;//新加入的代码
	return bn;
}

我们把之前二叉树遍历的 VisitBTNode 函数改为线索化函数,也就是在递归遍历二叉树的时候用这个函数访问每个节点,并实现二叉树的线索化。

在代码中,我们设置一个 pre 指针来记录遍历时候当前节点的上一个节点,每次遍历到一个节点时,就建立起它与前一个节点的前驱和后继关系(让前一个节点空闲的右指针指向它,让它的空闲的右指针指向前一个节点),再进行迭代,这样一来,当前节点的空闲右指针与它下一个节点的关系就会在迭代之后被当做 pre 节点的右指针与当前节点的关系进行操作

为了操作方便,pre指针我们设置为全局变量

BTNode* pre = NULL;//定义全局变量记录线索化时候的前驱结点

线索化函数

//访问二叉树的节点的函数(这里通过访问来进行线索化)
void VisitBTNode(BTNode* a)
{
	if (a->left == NULL)
	{
		a->left = pre;
		a->ltag = 1;
	}
	if (pre && pre->right == NULL) 
	{
		pre->right = a;
		pre->rtag = 1;
	}
	pre = a;
}

递归遍历函数也得改变(这里给出先序,中后序调换 VisitBTNode 位置既可)

//二叉树的先序遍历函数
void PerOrder(BTNode* node)
{
	if (node)
	{
		VisitBTNode(node);
		//加if的原因:线索化之后,二叉树的原本没有指向节点的指针指向了线索,
		//但是我们的遍历函数是针对没有线索的原本的二叉树写的,判断一下下一个要遍历的节点到底是不是孩子
		if (node->ltag == 0) PerOrder(node->left);
		if (node->rtag == 0) PerOrder(node->right);
	}
}

上面几个东西一改,二叉树的线索化差不多就完成了

写成一个线索化函数(先序为例)

void PerOrderBTree(BinaryTree* tree)
{
	if (tree->root) PerOrder(tree->root);
}

寻找后继节点与遍历二叉树的代码实现

根据前面目录章节的 “如何通过线索实现更简便的遍历” 一节,我们知道寻找后继节点无非就是 “叠if” ,然后通过找到的遍历的第一个节点+不断找到的后继节点就可以轻松遍历这棵二叉树了

先序

//先序遍历找到后继节点的函数
BTNode* Pernext(BTNode* node)
{
	if (node->left && node->ltag==0) return node->left;
	//左孩子不存在右孩子存在 & 左右孩子都不存在 两种情况用else统一
	else return node->right;
}

void PerOrder(BinaryTree* tree)//遍历先序线索化之后的二叉树
{
	if (tree && tree->root)
	{
		BTNode* node = tree->root;
		while (node)
		{
			printf("%c ", node->data);
			node = Pernext(node);
		}
	}
}

中序

这里把实现线索化的部分一起贴了过来

//二叉树的中序遍历函数(用于实现线索化版本)
void InOrder(BTNode* node)
{
	if (node)
	{
		if (node->ltag == 0) InOrder(node->left);
		VisitBTNode(node);
		if (node->rtag == 0) InOrder(node->right);
	}
}

void InOrderBTree(BinaryTree* tree)
{
	if (tree->root) InOrder(tree->root);
}

//获得某个节点的最左边的节点
BTNode* lfnode(BTNode* node)
{
	while (node->left && node->ltag == 0)
	{
		node = node->left;
	}
	return node;
}

//得到中序遍历时候某个节点的后继结点
BTNode* Innext(BTNode* node)
{
	if (node->rtag == 1) return node->right;
	else if (node->right) return lfnode(node->right);
	else return NULL;
}

//中序遍历线索化之后的树
void InOrder(BinaryTree* tree)
{
	if (tree && tree->root)
	{
		BTNode* node = lfnode(tree->root);
		while (node)
		{
			printf("%c ", node->data);
			node = Innext(node);
		}
	}
}

后序
后序遍历仿照前面的遍历其实不难写,但是需要再次对于节点定义加入一个指向父亲的指针,这里懒得写了

其实代码不重要,重要的是找后继节点的思路(为自己的偷懒找借口)(bushi)

最后把整个调试代码都贴上来

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <string.h>

// 获取当前时间(纳秒)
long long get_nanosec() {
	LARGE_INTEGER frequency;
	LARGE_INTEGER counter;
	QueryPerformanceFrequency(&frequency);
	QueryPerformanceCounter(&counter);
	return counter.QuadPart * 1000000000LL / frequency.QuadPart;
}

typedef struct BTNode { //二叉树结点     
	char data;//每个节点中存放的数据
	struct BTNode* left;
	struct BTNode* right;
	int ltag, rtag; //值为0表示指针指向孩子节点,值为1表示指向线索化的节点
}BTNode;

BTNode* pre = NULL;//定义全局变量记录线索化时候的前驱结点

typedef struct {//二叉树
	BTNode* root;
	int cnt; //节点数目
}BinaryTree;

//二叉树的节点创建函数
BTNode* CreateBTNode(char e) //传入一个数据,创建相应的节点
{
	BTNode* bn = (BTNode*)malloc(sizeof(BTNode));
	bn->data = e;
	bn->right = bn->left = NULL;
	bn->ltag = bn->rtag = 0;
	return bn;
}
//二叉树的初始化函数
BinaryTree* InitBinaryTree(BTNode* e)//传入一个节点,创建以该节点为根节点的树
{
	BinaryTree* bt = (BinaryTree*)malloc(sizeof(BinaryTree));
	bt->root = e;
	bt->cnt++;
	return bt;
}
//二叉树的节点插入函数(flag为1表示新节点为左孩子节点,为0表示新节点为右边孩子节点)
//child表示插入的节点,parent表示child节点的父节点
void InsertBTree(BinaryTree* tree, BTNode* child, BTNode* parent, int flag)
{
	if (flag == 1) parent->left = child;
	else parent->right = child;
	tree->cnt++;
}

//访问二叉树的节点的函数(这里通过访问来进行线索化)
void VisitBTNode(BTNode* a)
{
	if (a->left == NULL)
	{
		a->left = pre;
		a->ltag = 1;
	}
	if (pre && pre->right == NULL) 
	{
		pre->right = a;
		pre->rtag = 1;
	}
	pre = a;
}

//初始化一棵具体的树 
BinaryTree* Tree() {
	BTNode* a = CreateBTNode('A');
	BTNode* b = CreateBTNode('B');
	BTNode* c = CreateBTNode('C');
	BTNode* d = CreateBTNode('D');
	BTNode* e = CreateBTNode('E');
	BTNode* f = CreateBTNode('F');
	BTNode* g = CreateBTNode('G');
	BTNode* h = CreateBTNode('H');
	BTNode* k = CreateBTNode('K');
	BinaryTree* tree = InitBinaryTree(a);
	InsertBTree(tree, b, a, 1);
	InsertBTree(tree, e, a, 0);
	InsertBTree(tree, c, b, 0);
	InsertBTree(tree, d, c, 1);
	InsertBTree(tree, f, e, 0);
	InsertBTree(tree, g, f, 1);
	InsertBTree(tree, h, g, 1);
	InsertBTree(tree, k, g, 0);
	return tree;
}
//二叉树的先序遍历函数
void PerOrder(BTNode* node)(用于实现线索化版本)
{
	if (node)
	{
		VisitBTNode(node);
		//加if的原因:线索化之后,二叉树的原本没有指向节点的指针指向了线索,
		//但是我们的遍历函数是针对没有线索的原本的二叉树写的,判断一下下一个要遍历的节点到底是不是孩子
		if (node->ltag == 0) PerOrder(node->left);
		if (node->rtag == 0) PerOrder(node->right);
	}
}

void PerOrderBTree(BinaryTree* tree)
{
	if (tree->root) PerOrder(tree->root);
}

//先序遍历找到后继节点的函数
BTNode* Pernext(BTNode* node)
{
	if (node->left && node->ltag==0) return node->left;
	//左孩子不存在右孩子存在 & 左右孩子都不存在 两种情况用else统一
	else return node->right;
}

void PerOrder(BinaryTree* tree)//遍历先序线索化之后的二叉树
{
	if (tree && tree->root)
	{
		BTNode* node = tree->root;
		while (node)
		{
			printf("%c ", node->data);
			node = Pernext(node);
		}
	}
}

//二叉树的中序遍历函数(用于实现线索化版本)
void InOrder(BTNode* node)
{
	if (node)
	{
		if (node->ltag == 0) InOrder(node->left);
		VisitBTNode(node);
		if (node->rtag == 0) InOrder(node->right);
	}
}

void InOrderBTree(BinaryTree* tree)
{
	if (tree->root) InOrder(tree->root);
}

//获得某个节点的最左边的节点
BTNode* lfnode(BTNode* node)
{
	while (node->left && node->ltag == 0)
	{
		node = node->left;
	}
	return node;
}

//得到中序遍历时候某个节点的后继结点
BTNode* Innext(BTNode* node)
{
	if (node->rtag == 1) return node->right;
	else if (node->right) return lfnode(node->right);
	else return NULL;
}

//中序遍历线索化之后的树
void InOrder(BinaryTree* tree)
{
	if (tree && tree->root)
	{
		BTNode* node = lfnode(tree->root);
		while (node)
		{
			printf("%c ", node->data);
			node = Innext(node);
		}
	}
}

//二叉树的后序遍历函数(用于实现线索化版本)
void PostOrder(BTNode* node)
{
	if (node)
	{
		if (node->ltag == 0) PostOrder(node->left);
		if (node->rtag == 0) PostOrder(node->right);
		VisitBTNode(node);
	}
}

void PostOrderBTree(BinaryTree* tree)
{
	if (tree->root) PostOrder(tree->root);
}


int main() {
	long long start_time, end_time, execution_time;

	start_time = get_nanosec(); // 记录开始时间

	// 执行你的代码
	for (int i = 0; i < 100; i++)
	{
		BinaryTree* tree = Tree();

		PerOrderBTree(tree);
		printf("先序序列为:");
		PerOrder(tree);
		printf("\n");

		//InOrderBTree(tree);
		//printf("中序序列为:");
		//InOrder(tree);
		//printf("\n");

		/*	PostOrderBTree(tree);
			printf("后序序列为:");
			PostOrder(tree);*/
		printf("\n");
	}

	end_time = get_nanosec();   // 记录结束时间

	execution_time = end_time - start_time;  // 计算执行时间

	printf("程序执行时间:%lld 纳秒\n", execution_time);

	return 0;
}

拜拜

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值