“链式二叉树的实现”

前言

我们之前学习了树的基础概念以及二叉堆的实现,那末今天我们来实现二叉树的基本操作

1.存储结构

顺序结构

二叉树的顺序存储结构就是用一个一维数组来存储二叉树的节点,并且要保持节点之间的位置关系,那也就要求用数组的下标来体现节点之间的逻辑关系,比如:节点之间的父子关系,兄弟关系等。

以一颗完全二叉树为例,它的顺序结构如下图所示:
在这里插入图片描述
将这颗二叉树存入数组中,相应的下标对应上面的位置,如下图所示:
在这里插入图片描述

当然对于一般的二叉树来说,尽管层序编号不能反映逻辑关系,但是可以将其按完全二叉树编号,只不过,把不存在的节点设置为 “NULL” 而已。如下图,注意虚线结点表示不存在。

在这里插入图片描述

考虑一种极端的情况,一棵深度为 k 的右斜树,它只有 k 个结点,却需要分配 2^ k - 1
个存储单元空间,这显然是对存储空间的浪费,例如下图所示。
所以,顺序存储结构一般只用于完全二叉树

在这里插入图片描述

链式结构

既然顺序存储适用性不强,我们就要考虑链式存储结构。二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域 是比较自然的想法,我们称这样的链表叫做二叉链表。

结点结构图如下表所示。

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

其中,data 是数据域;left 和 right 都是指针域,分别存放指向左孩子和右孩子的指针。

逻辑图如下:

在这里插入图片描述

2.构建一颗二叉树

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。此处手动快速创建一棵简单的二叉树,快速进入二叉树操作学习,当代码需要调式改错时,也可以手搓一个二叉树。

代码示例:

BTNode* Buynode(BTDataType x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
	if (node == NULL)
	{
		perror("malloc tail");
		exit(-1);
	}
	node->data = x;
	node->left = NULL;
	node->right = NULL;
}

BTNode* BinaryTreeCreate()
{
	BTNode* node1 = Buynode(1);
	BTNode* node2 = Buynode(2);
	BTNode* node3 = Buynode(3);
	BTNode* node4 = Buynode(4);
	BTNode* node5 = Buynode(5);
	BTNode* node6 = Buynode(6);

	//手动连接节点
	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;

	return node1;
}

我们一般二叉树真正的创建方式是
通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
在这里插入图片描述
上面逻辑图是根据分治思路,递归实现的。其中限制条件是:对于当前节点,如果它是空节点或者数组已经遍历完,那么返回NULL。然后创建新节点赋初值当作根,接着递归创建左右子树。

代码示例:

BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	//其中,a表示前序遍历的数组,n表示数组的长度,pi表示数组的当前位置。
	if (*pi >= n || a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
//如果数组已经遍历完或者当前节点是空节点,那么返回空指针。
//否则,创建一个新节点,并将当前位置向后移动一位,然后递归构建左子树和右子树。
//最终返回根节点,即构建好的二叉树。
	BTNode* root = (BTNode*)malloc(sizeof(BTNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[*(pi)++];
	root->left = BinaryTreeCreate(a, n, pi);
	root->right = BinaryTreeCreate(a, n, pi);

	return root;
}

再看二叉树基本操作前,再回顾下二叉树的概念,二叉树是:

  1. 空树
  2. 非空:根结点,根结点的左子树、根结点的右子树组成的。
    在这里插入图片描述
    从概念中可以看出,二叉树定义是递归式的,因此后序基本操作中基本都是按照该概念实现的。

3.二叉树的遍历

在计算机程序中,遍历本⾝是⼀个线性操作。所以遍历同样具有线性结构的数组或链表,是⼀件轻⽽易举的事情。
在这里插入图片描述

对于⼆叉树来说,是典型的⾮线性数据结构,遍历时需要把⾮线性关联的节点转化成⼀个线性的序列,以不同的⽅式来遍历,遍历出的序列顺序也不同.

从节点之间位置关系的⾓度来看,⼆叉树的遍历分为 4 种:

1)前序遍历
2)中序遍历
3)后序遍历
4)层序遍历

从宏观的⾓度来看,⼆叉树的遍历归结为两⼤类:

1)深度优先遍历 (前序遍历、中序遍历、后序遍历)。
2)⼴度优先遍历 (层序遍历)。

深度优先遍历

深度优先和⼴度优先这两个概念不⽌局限于⼆叉树,它们更是⼀种抽象的算法思想,决定了访问某些复杂数据结构的顺序。在访问树、图,或其他⼀些复杂数据结构时,这两个概念常常被使⽤到。

所谓深度优先,顾名思义,就是偏向于纵深,“⼀头扎到底” 的访问⽅式。可能这种说法有些抽象,下⾯就通过⼆叉树的前序遍历、中序遍历、后序遍历 ,来看⼀看深度优先是怎么回事吧。

前序遍历

⼆叉树的前序遍历,输出顺序是:根节点、左⼦树、右⼦树。

规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。

在这里插入图片描述
代码示例:

void BinaryTreePrevOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
}

中序遍历

⼆叉树的中序遍历,输出顺序是:左⼦树、根节点、右⼦树。

规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。

在这里插入图片描述

⾸先访问根节点的左孩子,如果这个左孩子还拥有左孩子,则继续深⼊访问下去,⼀直找到不再有左孩子的节点,然后递归打印根节点。显然,第⼀个没有左孩子的节点是节点 4,然后继续递归找以4为根节点的左子树。一层一层往上回归。

代码示例:

void BinaryTreeInOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BinaryTreePrevOrder(root->left);
	printf("%d ", root->data);
	BinaryTreePrevOrder(root->right);
}

后序遍历

⼆叉树的后序遍历,输出顺序是:左子树、右子树、根节点。

规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。

在这里插入图片描述
代码示例:

void BinaryTreePostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	BinaryTreePrevOrder(root->left);
	BinaryTreePrevOrder(root->right);
	printf("%d ", root->data);

}

广度优先遍历

如果说深度优先遍历是在一个方向上 “一头扎到底”,那么广度优先遍历则恰恰相反:先在各个方向上各走出 1 步,再在各个方向上走出第 2 步、第 3 步…一直到各个方向全部走完。

层序遍历

层序遍历:设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第2层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。

在这里插入图片描述
上图就是⼀个⼆叉树的层序遍历,每个节点左侧的序号代表该节点的输出顺序。

这里我们需要借助之前学过的一个数据结构 队列 来实现层序遍历。

1)先把根入队列,借助队列,先进先出的性质。
2)上一层的节点出的时候,带下一层的节点进去。
3)一直重复,直到队列为空。
先入根节点,此时队列里面是有一个节点的,判断队列不为空,把根节点的数据取出来,然后在把根节点的左孩子节点和右孩子入进去,依次循环。
注意:上一个节点出来以后,把它下面的左右节点带进去。

代码示例:

//层序遍历
void LevelOrder(BTNode* root) {
	Queue q;
	QueueInit(&q);
	
	//队列不为空,入队列
	if (root) {
		QueuePush(&q, root);
	}

	//队列不为空,出队头的数据
	while (!QueueEmpty(&q)) {
		BTNode* front = QueueFront(&q); //取队头的数据
		QueuePop(&q); //然后Pop掉队列里面存的这个节点的指针

		printf("%d ", front->data); //然后访问data


		if (front->left) { //如果front的左子树不为空,那么就入队列
			QueuePush(&q, front->left);
		}

		if (front->right) { //如果front的右子树不为空,那么就入队列
			QueuePush(&q, front->right); 
		}
	}
}

4. 二叉树节点个数

这里可以采用 分治思路, 把复杂的问题,分成更小规模的子问题,子问题再分成更小规模的子问题,直到子问题不可再分割,直接能出结果。

思路:

1)如果是空树,那么就返回 0。
2)如果不是空树,那么就等于:求左子树节点个数+求右子树节点个数+1(这个 1 就是自己)。

代码示例:

int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}

	return BinaryTreeSize(root->left) + BinaryTreeSize(root->right) + 1;
}

5.二叉树叶子节点个数

所谓叶子节点,就是 没有任何 子节点 的节点称为叶子节点,也就是度为 0 的节点。

这里还是可以采用分治的思路:

1)若为空树,则叶子结点个数为 0。
2)若结点的左指针和右指针均为空,则叶子结点个数为 1(只剩一个根节点)。
3)除上述两种情况外,说明该树存在子树,其叶子结点个数 = 左子树的叶子结点个数 + 右子树的叶子结点个数。

代码示例:

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);
}

6.二叉树第 k 层节点个数

这里是求第 k 层的节点的个数,k > = 1 (k 从 1 开始的)
在这里插入图片描述

思路:

1)空树,返回 0。
2)不是空树,且 k==1,返回 1(求第一层的节点数,那么第一层只有一个根节点)。
3)不是空树,且 k>1,就转换成:左子树 k-1 层节点个数 + 右子树 k-1 层节点个数。

nt BinaryTreeLevelKSize(BTNode* root, int k)
{
	assert(k >= 1);

	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	return BinaryTreeLevelKSize(root->left,k - 1) + 
	BinaryTreeLevelKSize(root->right,k - 1);
}

7.二叉树的深度

求二叉树的深度,还是采用分治的思想:

1)若为空树,则深度为 0。
2)若不为空树,则深度 = 左右子树中深度最大的值 + 1(为什么要加1呢?因为还有第一层,也就是根节点这一层!)

在这里插入图片描述

注意要通过变量来记录左右子树递归的结果,否则后面代码会重复计算,导致效率低下,甚至程序崩溃。

代码示例:

nt TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return leftHeight > rightHeight ?
		leftHeight + 1 : rightHeight + 1;
}

8.二叉树查找值为 x 的节点

二叉树查找值为 x 的节点,然后返回节点的指针。

思路:

1)如果是空树,那么直接返回空。
2)先判断根节点的值是不是要查找的x,如果是,直接返回。
3)如果根节点不是,那么去左子树中查找。
4)如果左子树找不到,那么再去右子树中查找。

代码示例:

//二叉树查找值为x的节点
BTNode* BTreeFind(BTNode* root, BTDataType x) {
	//如果根节点为空树,那么就直接返回空
	if (root == NULL) {
		return NULL;
	}

	//如果当前的data等于x,那么就返回当前节点指针
	if (root->data == x) {
		return root;
	}

	//如果当前节点不是,那么就去左边找,找到了就返回
	BTNode* ret1 = BTreeFind(root->left, x);
	if (ret1) {
		return ret1;
	}

	//如果左边找不到,就去右边找,找到了就返回
	BTNode* ret2 = BTreeFind(root->right, x);
	if (ret2) {
		return ret2;
	}

	//如果左边和右边都没有找到,那么就返回空
	return NULL;
}

9.二叉树的销毁

二叉树需要注意销毁结点的顺序,遍历时我们选用后序遍历,也就是说,销毁顺序应该为:左子树、右子树、根。

我们必须先将左右子树销毁,最后再销毁根结点;若先销毁根结点,那么其左右子树就无法找到,也就无法销毁了。

代码示例:

void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL);
	{
		return;
	}

	BinaryTreeDestory(root->left);
	BinaryTreeDestory(root->right);
	free(root);
}

10.判断二叉树是否是完全二叉树

什么是完全二叉树?

前 n-1 层都是满的,最后一层不满,但是从左到右依次是连续的。

这里还是采用 队列 的思路:

1)层序遍历,空节点也进队列。
2)出到空节点以后,出队列中的所有数据,如果全是空,就是完全二叉树,如果有非空,就不是。

在这里插入图片描述
完全二叉树:非空节点都出完了,那么遇到空以后,队列后面肯定全是空。

代码示例:

//判断二叉树是否是完全二叉树
bool BTreeComplete(BTNode* root) {
	Queue q;
	QueueInit(&q);

	//队列不为空,入队列
	if (root) {
		QueuePush(&q, root);
	}

	//队列不为空,出队头的数据
	while (!QueueEmpty(&q)) {
		BTNode* front = QueueFront(&q); //取队头的数据
		QueuePop(&q);

		//如果出到空,那么就跳出循环,进入下一个循环,判断后面还有没有数据?
		if (front == NULL) {
			break;
		}
	
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}

	//出break以后,去判断
	while (!QueueEmpty(&q)) {
		BTNode* front = QueueFront(&q); //取队头的数据
		QueuePop(&q);

		//如果出到非空,那么说明不是完全二叉树
		if (front) {
			return false;
		}
	}

	//销毁队列
	QueueDestory(&q);
	return true;
}

总结

以上就是关于初阶二叉树的基本操作,那么这些的学习都是为了后面的二叉查找树、平衡二叉树、红黑树、B 树以及B+树打基础。

  • 36
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Filex;

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值