线索二叉树

文章介绍了二叉树的线索化过程,通过在二叉链表中利用空指针域存储前驱或后继结点的地址,以便在中序、先序和后序遍历时能更方便地找到相应结点。代码示例展示了中序和先序线索化二叉树的实现以及如何找到结点的前驱和后继。
摘要由CSDN通过智能技术生成

如果使用二叉链表来实现二叉树,每个结点就有两个指针域,分别指向其左孩子和右孩子。对于有n个结点的二叉树来说,总共就有2*n个指针域,但是二叉树中除过根节点外,每一个结点只需要一个指针来链接,也就是说,对于n个结点的二叉树,我们只使用了其中n-1个指针域,其余n+1个指针都是空指针,这就浪费了大量的空间。我们可以使用这些空着的指针域来存储该结点的前驱或是后继结点的地址,这就是二叉树的线索化。在写代码的时候一定要注意判断左右指针指向的是前驱后继还是孩子结点,否则就会出现错误。

中序线索化二叉树:

//中序线索二叉树
ClueTreeNode* InOrderClue(ClueTreeNode* root) {
	static ClueTreeNode* pre = NULL;
	if (root == NULL) {
		if (pre) {
			if (pre->right == NULL)
				pre->rightflag = 1;
		}
	}
	else {
		//先线索化左子树,由于root此时还没处理,所以不需要像先序一样判断左指针是否为孩子
		InOrderClue(root->left);
		//再线索化根节点
		if (root->left == NULL) {
			root->leftflag = 1;
			root->left = pre;
		}
		if (pre) {
			if (pre->right == NULL) {
				pre->rightflag = 1;
				pre->right = root;
			}
		}
		pre = root;
		//最后线索化右子树,右指针不需要判断,因为只有当root作为pre时才可能修改右指针
		InOrderClue(root->right);
	}
	return root;
}

//由于中序遍历是左根右的顺序,所以对于一个结点来说有三种情况
//1.有两个孩子,那么它的前驱肯定是左子树的最右结点,因为左子树也是左根右的顺序,后继是右子树的最左结点
//2.有一个孩子,如果是左孩子,那么前驱与上面相同,如果是右孩子,那么左指针指向前驱,后继刚好相反
//3.没有孩子,左指针指向前驱,右指针指向后继
ClueTreeNode* InOrderFindPre(ClueTreeNode* root, int k) {
	//首先找到k结点
	ClueTreeNode* tmp = root;
	while (tmp && tmp->val != k) {
		if (tmp->val > k)
			if (tmp->leftflag == 0)
				tmp = tmp->left;
			else
				tmp = NULL;
		else
			if (tmp->rightflag == 0)
				tmp = tmp->right;
			else
				tmp = NULL;
	}
	if (tmp == NULL) {
		printf("指定节点不存在\n");
		return NULL;
	}
	if (tmp->leftflag)
		return tmp->left;
	else {
		ClueTreeNode* ttmp = tmp->left;
		while (ttmp->rightflag == 0)
			ttmp = ttmp->right;
		return ttmp;
	}
}

ClueTreeNode* InOrderFindNext(ClueTreeNode* root, int k) {
	ClueTreeNode* tmp = root;
	while (tmp && tmp->val != k) {
		if (tmp->val > k)
			if (tmp->leftflag == 0)
				tmp = tmp->left;
			else
				tmp = NULL;
		else
			if (tmp->rightflag == 0)
				tmp = tmp->right;
			else
				tmp = NULL;
	}
	if (tmp == NULL) {
		printf("指定节点不存在\n");
		return NULL;
	}
	if (tmp->rightflag)
		return tmp->right;
	else {
		ClueTreeNode* ttmp = tmp->right;
		//由于找后继需要在右子树中找最左结点,但是左指针可能是前驱
		//所以当左标记为孩子结点时需要继续向左找,否则停止
		while (ttmp->leftflag == 0)
			ttmp = ttmp->left;
		return ttmp;
	}
}

//根据中序线索二叉树遍历
void TraverseInOrderClueBinaryTree(ClueTreeNode* root) {
	if (root == NULL)
		return;
	//首先找到第一个结点,在中序线索二叉树中,左指针不论是指向孩子还是前驱,第一个结点都在最左侧
	ClueTreeNode* tmp = root;
	while (tmp->left)
		tmp = tmp->left;
	//开始遍历,中序遍历是左根右的顺序,所以每次遍历的子树最后都会停在该右子树的叶节点
	//那么就一定可以拿到该结点的后继
	while (tmp) {
		printf("%d ", tmp->val);
		tmp = tmp->right;
	}
}

先序线索化二叉树:

ClueTreeNode* Insert(ClueTreeNode* root, int k) {
	if (root == NULL) {
		ClueTreeNode* tmp = (ClueTreeNode*)malloc(sizeof(ClueTreeNode));
		tmp->leftflag = tmp->rightflag = 0;
		tmp->left = tmp->right = NULL;
		tmp->val = k;
		return tmp;
	}
	else {
		if (root->val > k)
			root->left = Insert(root->left, k);
		else
			root->right = Insert(root->right, k);
		return root;
	}
}

//先序遍历线索化
ClueTreeNode* PreOrderClue(ClueTreeNode* root) {
	static ClueTreeNode* pre = NULL;
	if (root == NULL) {
		if (pre) {
			if (pre->right == NULL) {
				pre->rightflag = 1;
			}
		}
		return NULL;
	}
	else {
		if (root->left == NULL) {
			root->leftflag = 1;
			root->left = pre;
		}
		if (pre) {
			if (pre->right == NULL) {
				pre->rightflag = 1;
				pre->right = root;
			}
		}
		pre = root;
		//由于在此之前根已经进行线索化,所以必须判断左指针指向的是否是孩子结点
		if (root->leftflag == 0)
			PreOrderClue(root->left);
		PreOrderClue(root->right);
	}
	return root;
}

//如果left指向前驱,那么可以直接得到前驱,如果没有指向,那么由于先序遍历的顺序,
//要找前驱必须得到其父节点,如果使用二叉树,这是不好实现的,所以对于先序线索二叉树
//我们可以找到指定节点的后继
ClueTreeNode* PreOrderFindNext(ClueTreeNode* root, int k) {
	//首先根据二叉搜索树的特点找到k结点
	ClueTreeNode* tmp = root;
	while (tmp && tmp->val != k) {
		if (tmp->val > k)
			if (tmp->leftflag == 0)
				tmp = tmp->left;
			else
				tmp = NULL;
		else
			if (tmp->rightflag == 0)
				tmp = tmp->right;
			else
				tmp = NULL;
	}
	if (tmp == NULL) {
		printf("指定节点不存在\n");
		return NULL;
	}
	//如果rightflag==1,那么right指向的就是后继
	if (tmp->rightflag)
		return tmp->right;
	else {
		//根据根左右的顺序,如果有左子树,那么后继为左子树的根节点,否则为右子树的根结点
		if (tmp->leftflag == 0)
			return tmp->left;
		else
			return tmp->right;
	}
}

//根据先序线索二叉树遍历
void TraversePreOrderClueBinaryTree(ClueTreeNode* root) {
	if (root == NULL)
		return;
	//因为先序线索二叉树可以直接找到后继,所以可以使用循环直接完成遍历
	ClueTreeNode* tmp = root;
	while (tmp) {
		printf("%d ", tmp->val);
		if (tmp->leftflag == 0)
			tmp = tmp->left;
		else
			tmp = tmp->right;
	}
}

后序线索化二叉树:

//后序遍历的顺序是左右根,根是最后才访问的,假设我们建立好了后序线索二叉树
//在遍历线索二叉树时,如果遇到了有两个孩子的结点,在遍历完这个结点后,
//我们需要找到该结点的后继,也就是该结点的父节点的右子树的最左结点。但是,
//在二叉树中,我们没有办法通过这个结点直接找到它的父节点,除非多增加一个指针域
//指向其父节点,但是找到其父节点后,我们仍然要通过循环找到父节点右子树的最左下结点
//而线索二叉树本身是帮助我们更方便的遍历二叉树的,但是后序线索二叉树明显使得遍历
//更加的麻烦了,在遍历时要不断处理一些特殊情况,没有办法很方便的拿到后继,所以,
//实现后序线索二叉树的意义就不是很大。但其实现依然是使用递归。

测试代码: 

#include <stdio.h>
#include <stdlib.h>

//对于n个结点的二叉树,共有2*n个指针域,其中n+1个指针域都是空的
//那么就可以使用这些空着的指针域来指向该结点的前驱或者后继
typedef struct ClueTreeNode {
	int val;
	int leftflag;//用于标记左指针是前驱还是孩子
	int rightflag;//1表示为前驱或是后继
	struct ClueTreeNode* left;
	struct ClueTreeNode* right;
}ClueTreeNode;

int main() {

	//通过插入的方式建立一个二叉搜索树
	ClueTreeNode* root = NULL;
	root = Insert(root, 5);
	for (int i = 0; i < 5; i++)
		root = Insert(root, i);
	for (int i = 6; i < 10; i++)
		root = Insert(root, i);

	root = InOrderClue(root);
	/*for (int i = 0; i < 10; i++) {
		ClueTreeNode* tmp = PreOrderFindNext(root, i);
		if (tmp) {
			printf("结点%d的后继是:%d\n", i, tmp->val);
		}
		else {
			printf("该结点没有后继\n");
		}
	}
	TraversePreOrderClueBinaryTree(root);
	printf("\n----------------------\n");
	Pre(root);*/

	for (int i = 0; i < 10; i++) {
		ClueTreeNode* tmp = InOrderFindNext(root, i);
		if (tmp) {
			printf("结点%d的后继是:%d\n", i, tmp->val);
		}
		else {
			printf("该结点没有后继\n");
		}
	}
	for (int i = 0; i < 10; i++) {
		ClueTreeNode* tmp = InOrderFindPre(root, i);
		if (tmp) {
			printf("结点%d的前驱是:%d\n", i, tmp->val);
		}
		else {
			printf("该结点没有前驱\n");
		}
	}
	TraverseInOrderClueBinaryTree(root);
	printf("\n----------------------\n");
	In(root);

	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值