数据结构:实现链式结构二叉树

我们之前学习了树的遍历,体会到了递归的暴力美学。现在我们来实现剩下的二叉树操作,关于二叉树,我们知道一般有不止一个叶子结点,所以这里不讲删除和插入结点的操作,因为可以插入和删除结点的位置太多了,没有什么意义,后面讲红黑树的时候会学到。

头文件声明

cpp 复制代码
#pragma once

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

typedef char BTDataType;//类型重命名
//二叉树结点的结构
typedef struct BinaryTreeNode
{
    BTDataType data;//数据域
    struct BinaryTreeNode* left;//指向左孩子的指针
    struct BinaryTreeNode* right;//指向右孩子的指针
}BTNode;

//先序遍历
void PreOrder(BTNode* root);
//中序遍历
void InOrder(BTNode* root);
//后序遍历
void PostOrder(BTNode* root);

//二叉树结点个数
int BinaryTreeSize(BTNode* root);

//叶子结点个数
int BinaryTreeLeafSize(BTNode* root);

//二叉树高度
int BinaryTreeLevel(BTNode* root);

//二叉树第k层结点个数
int BinaryTreeLevelkSize(BTNode* root, int k);

//查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);

//销毁二叉树
void BinaryTreeDestroy(BTNode** root);

//层序遍历
void LevelOrder(BTNode* root);

//判断是否为完全二叉树
bool CompleteBinaryTree(BTNode* root);

我们每个函数实现后都用之前创建好的二叉树来进行简单功能测试:

同时因为之前在讲树的遍历的时候有说明过详细递归过程,这里不再赘述,可以参考我之前发布的博客:数据结构:树的遍历

二叉树结点个数

二叉树的总结点个数=根结点+左子树结点个数+右子树结点个数。对于递归式结构的二叉树来说,我们就可以去递归遍历它的左右子树,如果结点不存在则返回0。

代码来说就很简单了:

cpp 复制代码
//二叉树结点个数
int BinaryTreeSize(BTNode* root)
{
	//如果结点不存在则直接返回0
	if (root == NULL)
	{
		return 0;
	}
	return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

往下递归它的左右子树,并返回结点个数,这个代码就完成了。我们测试一下:

结果与预期符合,说明代码逻辑没什么问题。

二叉树叶子结点个数

我们知道,叶子结点就是度为0(也可以说没有左右孩子结点)的结点。

叶子结点的个数=左子树叶子结点个数+右子树叶子结点个数。

所以我们还是左右子树往下递归去查找,我们传进去一个根结点,保证它存在,如果它的左右结点都不存在(为NULL),那么就说明这个结点是一个叶子结点,返回1;如果根结点不存在,那就直接返回0,没有查找的必要了。

函数代码如下:

cpp 复制代码
//叶子结点个数
int BinaryTreeLeafSize(BTNode* root)
{
	//根结点不存在,无需往下查找
	if (root == NULL)
		return 0;
	//根结点存在,判断是否为叶子结点
	if (root && root->left == NULL && root->right == NULL)
		return 1;
	//返回左右子树叶子结点之和
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

我们来测试一下:

结果符合预期,代码逻辑没有什么问题。

二叉树的高度

二叉树的高度(深度),实际上就是根结点+左右子树(高的那个)的高度。

按图来理解就是这样的:

因为要比较左右子树的高度,所以我们需要两个变量left_level和right_level来存储左右子树的高度,然后max来存储高度比较高那个值,和根结点相加作为二叉树的高度返回来。

同样的,如果根结点不存在,我们则返回0。

代码如下:

cpp 复制代码
//二叉树高度
int BinaryTreeLevel(BTNode* root)
{
	if (root == NULL)
		return 0;
	//存储左右子树高度
	int left_level = BinaryTreeLevel(root->left);
	int right_level = BinaryTreeLevel(root->right);
	//比较哪个高度高
	int max = left_level > right_level ? left_level : right_level;
	//返回二叉树高度
	return 1 + max;
}

我们简单测试一下:

结果符合预期,代码逻辑没什么问题。

二叉树第k层的结点个数

二叉树第k层结点个数可以这样找,因为根结点是第一层,所以第k层的结点个数就是根结点的左右子树第k-1层的结点个数,左右子树一直往下递归,直到变成左右子树第1层的结点个数,就可以找到第k层的结点个数了。

如果是第一层,那么就是根结点,返回1,如果根结点不存在,直接返回0。

代码如下:

c 复制代码
//二叉树第k层结点个数
int BinaryTreeLevelkSize(BTNode* root, int k)
{
   //边界防护
   if(k<=0)
   return 0;
	if (k == 1 && root)
		return 1;
	if (root==NULL)
		return 0;
	
	return BinaryTreeLevelkSize(root->left, k-1) 
		+ BinaryTreeLevelkSize(root->right, k-1);
}

我们简单测试一下:

结果符合预期,代码逻辑没什么问题。

查找值为x的结点

查找这个结点,先从根结点开始,如果根结点不是则往左右子树开始遍历,如果左子树中找到了则不用遍历右子树,返回该结点指针;没找到则遍历右子树看是否能找到,都找不到则说明该结点不存在,返回NULL。

如果根结点为NULL,则不用查找了,直接返回NULL。

代码如下:

c 复制代码
//查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root->data == x)
		return root;
	if (root == NULL)
		return NULL;
	//根结点不是,从左右子树中去找
	BTNode* LeftFind = BinaryTreeFind(root->left, x);
	//找到则返回该结点,停止从右子树中去查找
	if (LeftFind)
		return LeftFind;
	BTNode* RightFind = BinaryTreeFind(root->right, x);
	if (RightFind)
		return RightFind;
	//都找不到则返回NULL
	return NULL;
}

我们简单测试一下:

结果符合预期,说明代码没什么问题。

二叉树的销毁

因为我们的二叉树是由一个个结点组成的,每个结点都是一个一级指针,要销毁二叉树,我们就需要传过去根结点的地址,也就是一个二级指针。(希望形参的改变可以影响实参,释放后要把root置为NULL)

如果我们先释放根结点的内存,那么就找不到它的左右子树并释放了,所以我们可以按左右根的顺序销毁二叉树,跟后序遍历一样。如果根结点为空,则说明二叉树为空,销毁完成,直接返回。

代码如下:

c 复制代码
/销毁二叉树
void BinaryTreeDestroy(BTNode** root)
{
	if (*root == NULL)
		return;
	//先释放左右子树
	//注意传过来的是二级指针
	BinaryTreeDestroy(&(*root)->left);
	BinaryTreeDestroy(&(*root)->right);
	//最后释放根结点
	free(*root);
	*root = NULL;
}

层序遍历

层序遍历就是按顺序遍历打印每层的结点。

如这棵二叉树,按层序遍历顺序就是:

A B C D NULL E F G NULL NULL NULL NULL NULL NULL NULL NULL(NULL也可以不打印)

那么如何来实现呢?按照层序遍历顺序来看,我们打印完根结点要拿到根结点的左孩子结点,打印完再拿到根结点的右孩子结点。打印完再拿到B的左孩子,然后右孩子,然后是C的左右孩子一直到遍历完整个二叉树。

要达成层序遍历,我们就需要借助数据结构:队来实现。

实现思路是这样的,我们先把根结点入队,然后拿到它的左右孩子,不为空则入队列。

然后我们取队头元素,打印队头元素,然后执行出队操作,把A出队。

然后取队头结点B,将B的左右孩子(不为空)入队。

再把B打印一下,然后执行出队操作:

然后再取队头,将队头的左右孩子入队列,再打印队头,然后出队,这样一直循环往复,直到队列为空,层序遍历就结束了。

我们把之前实现队列的接口函数的文件添加进去,将队列中存放的元素类型改为结点类型BTNode*,再包含一下头文件。该函数先定义一个队列,初始化和销毁中间写主要思路。

我们先把根结点入队,保证队列不为空(为空则不会执行下列循环),队列不为空则执行以下循环操作:取队头、入队头左右结点、打印队头、出队。直到队列为空时跳出循环,层序遍历结束。

代码实现如下:

c 复制代码
//层序遍历
void LevelOrder(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	//根结点入队
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		//取队头
		BTNode* top = QueueHead(&q);
		//入左右孩子结点
		if (top->left)
			QueuePush(&q, top->left);
		if (top->right)
			QueuePush(&q, top->right);
		//打印队头然后出队
		printf("%c ", top->data);
		QueuePop(&q);
	}
printf("\n");
	QueueDestroy(&q);
}

我们简单测试一下:

结果符合预期,代码逻辑没什么问题。

判断是否为完全二叉树

我们知道,完全二叉树的结点和序号是一一对应 的关系,要判断是否是完全二叉树,同样地,我们也要借助队列来实现,但和层序遍历有所区分的是,不管左右结点是否为空,我们都要入队列。

比如说这棵二叉树,很明显不是一棵完全二叉树,我们来看入队列的过程:

先入根结点,然后我们入队头的左右结点,取出根结点:

继续入队头的左右结点,取出队头:

下一次取到的就是NULL了,此时我们结束第一次循环,进入第二次取队头循环:

我们会发现第二次循环中取到了不为空的结点。

接下来我们举个完全二叉树的例子:

第一次循环中入队头左右结点,出队头,一直到遇到结点为空的情况是这样的:

此时队头为空,跳出第一次循环,进入第二次循环取队头,发现全取出来了,没有遇到结点不为空的情况。

从以上两个例子中我们可以看出来,完全二叉树和非完全二叉树在第一次循环中没有差别,重点在于第二次循环,完全二叉树取队头不会遇到队头不为空的情况,而非完全二叉树会。

所以在第二次循环中,如果遇到top!=NULL的情况,则说明该树是非完全二叉树,返回false。第二次循环顺利结束,则说明是完全二叉树,返回true。

这两次循环条件都是队列不为空。

代码如下:

c 复制代码
//判断是否为完全二叉树
bool CompleteBinaryTree(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	//根结点入队
		QueuePush(&q, root);
	//第一次循环
	while (!QueueEmpty(&q))
	{
		//取队头
		BTNode* top = QueueHead(&q);
		//取到队头为空,跳出循环
		if (top == NULL)
			break;
		//入左右孩子结点
			QueuePush(&q, top->left);
			QueuePush(&q, top->right);
		QueuePop(&q);
	}
	while (!QueueEmpty(&q))
	{
		//取队头
		BTNode* top = QueueHead(&q);
		//取到队头不为空,说明是非完全二叉树
		if (top != NULL)
			return false;
		QueuePop(&q);
	}
	QueueDestroy(&q);
}

我们简单测试一下:

结果符合预期,代码没什么问题。

相关推荐
_Narcissus_1 小时前
动态规划初探(含完整代码实现)
c语言·数据结构·c++·算法·动态规划·背包问题·最短路径
_Narcissus_3 小时前
枚举和模拟算法笔记
c语言·数据结构·c++·笔记·算法·模拟·枚举
冻柠檬飞冰走茶3 小时前
《数据结构实验指导-C++语言版》 在顺序表 list 中查找元素 x
开发语言·数据结构·c++·算法·list
Brilliantwxx3 小时前
【C++】 高阶数据结构图(1)并查集
开发语言·数据结构·c++
冻柠檬飞冰走茶4 小时前
《数据结构实验指导-C++语言版》 返回单链表 list 中第 i 个元素值
开发语言·数据结构·c++·算法·list
LuminousCPP6 小时前
栈和队列专题(四):LeetCode 232. 用栈实现队列|双栈分工 + 按需迁移 + 摊还 O(1)
c语言·数据结构·笔记·算法·leetcode
纪念 2296 小时前
二叉树排序讲解(一)
数据结构
tudousisi2226 小时前
01背包8.21
数据结构·算法
石头猫灯16 小时前
WordPress wp2shell 漏洞链完整拆解流程
网络·数据结构·安全·web安全