二叉树链式结构的实现

发布于:2023-01-20 ⋅ 阅读:(12) ⋅ 点赞:(0) ⋅ 评论:(0)

上期我们学习了二叉树的堆,这期让我们来看看二叉树的结构,以及如何实现。

在这之前先补充上期没有说到的二叉树的链式存储:
链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是 链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所 在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面 学到高阶数据结构如红黑树等会用到三叉链。

二叉树链式结构的实现:

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入,此处手动快速创建一棵简单的二叉树,快速进入二叉树 操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

下面用代码的方式展示:假设我们要创建的是如图二叉树

我们可以用以下代码实现:

//手动创造出一颗二叉树
BTNode* BinaryTreeCreate()
{
	BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
	assert(n1);
	BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
	assert(n2);
	BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
	assert(n3);
	BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
	assert(n4);
	BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
	assert(n5);
	BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
	assert(n6);
	//给节点赋值
	n1->data = 1;
	n2->data = 2;
	n3->data = 3;
	n4->data = 4;
	n5->data = 5;
	n6->data = 6;

	//建立链接关系
	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n2->right = NULL;
	n3->left = NULL;
	n3->right = NULL;
	n4->left = n5;
	n4->right = n6;
	n5->left = NULL;
	n5->right = NULL;
	n6->left = NULL;
	n6->right = NULL;
	return n1;
}

 二叉树的遍历:(前序、中序以及后序遍历)

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉 树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历 是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

1. 前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。

2. 中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。

3. 后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。

由于被访问的结点必是某子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为 根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。

我们先看看这三种遍历的结果:

 

上图中一个矩形代表一棵树,通过其打印的前后顺序就可以看出来递归的特点。 

二叉树的遍历我们要使用递归的方式去遍历下面是递归的代码:

// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root)///根 左子树 右子树
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root)//左子树  根  右子树
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BinaryTreeInOrder(root->left);
	printf("%d ", root->data);
	BinaryTreeInOrder(root->right);
}
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root)//左子树  右子树  根
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BinaryTreePostOrder(root->left);
	BinaryTreePostOrder(root->right);
	printf("%d ", root->data);
}

下面我就通过画函数栈帧的图来说明这是如何遍历的:(这里以前序遍历为例)

 二叉树的其他接口:

对于这些接口,我们的整体的思路是分而治之,就是大事化小,小事化了。

一般我们都可以画图解决,先想好每一步该怎么走,怎么分配到子树。对于这种问题,我个人认为,从最后一层开始思考是比较好找到解决方案的,因为最后一层最接近底层,就可以不断往上走,根据所需去解决。

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	//返回左边的和右边的还有自己
	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
    //判断叶子节点
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelKSize(root->left, k-1) + BinaryTreeLevelKSize(root->right, k-1);
}

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	//看根的左子树有没有
	BTNode* left = BinaryTreeFind(root->left, x);
	if (left)
		return left;
	//看根的右子树有没有
	BTNode* right = BinaryTreeFind(root->right, x);
	if (right)
		return right;
	//都没有
	return NULL;
}

//二叉树的高度
int TreeHeight(BTNode* root)
{
	//左子树和右子树的和加根自己也就是加1就是结果
	if (root == NULL)
	{
		return 0;
	}
	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreateByPrev(BTDataType* a, int* pi)
{
	//遵守根左子树右子树的规则,从最后一层开始链接起来
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	root->data = a[(*pi)++];
	//链接根的左右
	root->left = BinaryTreeCreateByPrev(a, pi);
	root->right = BinaryTreeCreateByPrev(a, pi);
	return root;
}

//二叉树的销毁
void BinaryTreeDestory(BTNode* root)
{
	//递归到最后一个再不断向上销毁
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestory(root->left);
	BinaryTreeDestory(root->right);
	free(root);
}

不用递归的两个接口:

一个是层序遍历,另一个是判断一棵树是否为满二叉树。

首先先介绍什么是层序遍历:
层序遍历:除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根节点所在 层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层 上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

通过以上方式打印出来的就是层序遍历,那我们该如何实现呢?

很容易看出来这里用递归的方式不合适,因为递归是要递归到最后的,而这里的打印就不能递归到最后解决,这里可以利用队列的性质,根入队列后就记录打印一下,出队列的时候如该根的左右子树,这样最后的顺序就是正确的,让我们看看代码的实现方式:

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	//利用队列的性质,先将根的左右子树进队,然后每次出队的时候都进出队的那个元素的两个子树
	Qe q;
	QueueInit(&q);
	//先讲根进队列
	QueuePush(&q,root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		printf("%d ", front->data);
		QueuePop(&q);
		//出一个根进根的左右子树
		if (front->left)
		{
			QueuePush(&q, front->left);
		}
		if (front->right)
		{
			QueuePush(&q, front->right);
		}
	}
}

 接下来就是和层序遍历的思想很像的判断一棵树是否为完全二叉树:主要思路也是利用队列来实现,和层序遍历相似,把全部元素入队列,如果遇到空指针,并且空指针的后面还有其他元素则说明这不是完全二叉树,否则就是完全二叉树,下面我们用代码来实现:

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	//利用层序遍历的思想,如果最后出到空之后的元素中还存在非空元素,就不是完全二叉树
	Qe q;
	QueueInit(&q);
	//先讲根进队列
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		//出到空指针就跳出去
		if (front == NULL)
		{
			break;
		}
		//出一个根进根的左右子树,这里空也要进去,因为后面要判断
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
	//到这里所有元素都进入了并且如果是完全二叉树的话就只剩空
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestroy(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

最后在出函数前不要忘记销毁队列,防止内存泄漏。

到这里二叉树的学习就暂时到一段落了,在学习二叉树的时候,尤其是递归,如果不会一定要画图分析,因为不可能有正确的代码给你画函数栈帧。