用C语言把一棵普通二叉树安排得明明白白

1. 树的相关术语

结点的度:一个结点含有的子树的个数称为该结点的度; 如上图:A的为6

叶结点或终端结点:度为0的结点称为叶结点; 如上图:B、C、H、I...等结点为叶结点

非终端结点或分支结点:度不为0的结点; 如上图:D、E、F、G...等结点为分支结点

双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点

孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点

兄弟结点:具有相同父结点的结点互称为兄弟结点; 如上图:B、C是兄弟结点

树的度:一棵树中,最大的结点的度称为树的度; 如上图:树的度为6

结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;

树的高度或深度:树中结点的最大层次; 如上图:树的高度为4

堂兄弟结点:双亲在同一层的结点互为堂兄弟;如上图:H、I互为兄弟结点

结点的祖先:从根到该结点所经分支上的所有结点;如上图:A是所有结点的祖先

子孙:以某结点为根的子树中任一结点都称为该结点的子孙。如上图:所有结点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林;

满二叉树:深度为k且有2^{k}-1个结点的二叉树。在满二叉树中,每层结点都是满的,即每层结点都具有最大结点数。

满二叉树的顺序表示:从二叉树的根开始,按层间从上到下,层内从左到右的顺序进行编号(0,1,2,……,n-1)。

完全二叉树:深度为k,结点数为n的二叉树,如果其结点0~n-1的位置序号分别与等高的满二叉树的结点1~n-1的位置序号一一对应,则为完全二叉树。完全二叉树的特点为:(1)叶子结点只可能出现在最后两层;(2)度数为1的结点个数为0或1。

 2. 二叉树的性质

性质1:在二叉树的第i层上至多有2^{i-1}个结点(i>=1)

性质2:深度为k的二叉树至多有2^{k}-1个结点(k>=1)

性质3:对任意一棵二叉树T,若终端结点数为n_{1},而其度数为2的结点数为n_{2},则                             n_{1} = n_{2}+1

性质4:具有n个结点的完全二叉树的深度为(log_{2}n)+1

性质5:对于具有n个结点的完全二叉树,如果按照从上到下,从左到右的顺序对完全二叉树               中的所有结点从0开始编号,则对于任意序号为i的结点,其双亲结点的序号为                         (i-1)/2,左孩子的序号为2*i+1,右孩子的序号为2*i+2。

证明略,但是上面的性质5在堆中有较为重要价值,在上一篇文章中有证明:全面详解堆-CSDN博客 

1. 二叉树的声明以及要实现的接口

typedef char BTDataType;

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

// 通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi);
// 二叉树销毁
void BinaryTreeDestory(BTNode** root);
// 二叉树节点个数
int BinaryTreeSize(BTNode* root);
// 二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root);
// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k);
// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x);
// 二叉树前序遍历 
void BinaryTreePrevOrder(BTNode* root);
// 二叉树中序遍历
void BinaryTreeInOrder(BTNode* root);
// 二叉树后序遍历
void BinaryTreePostOrder(BTNode* root);
// 层序遍历
void BinaryTreeLevelOrder(BTNode* root);
// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root);
//树的高度
int TreeHeight(BTNode* root);

2. 通过前序遍历数组构建二叉树

在对二叉树进行操作之前,我们需要有一棵二叉树。

根据二叉树的用途不同,在二叉树中插入元素的方式繁多且复杂,在初学阶段不需要太多的了解。

于是,我们可以采用暴力构建二叉树的方式,也就是手动实现结点之间的相互链接关系:

BTNode* BuyNode(BTDataType x)
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->left = NULL;
	newnode->right = NULL;

	return newnode;
}

BTNode* CreatBinaryTree()
{
	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##"构建二叉树
BTNode* BinaryTreeCreate(BTDataType* a, int n, int* pi)
{
	if (*pi >= n || a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}
	else
	{
		BTNode* root = BuyNode(a[*pi]);
		(*pi)++;
		root->left = BinaryTreeCreate(a, n, pi);
		root->right = BinaryTreeCreate(a, n, pi);
		return root;
	}
}

a为存放前序遍历序列的数组,'#'表示当前结点为空指针;

n表示数组a的元素个数;

pi为表示当前数组下标的变量的指针,初始值为0,每有一个元素加入到树中,(*pi)++。

3. 二叉树销毁

 这里采用的是后序遍历来释放结点,否则需要定义变量来记录左右子树的根结点。

// 二叉树销毁
void BinaryTreeDestory(BTNode** root)
{
	if (*root == NULL)
		return;
	else
	{
		BinaryTreeDestory(&((*root)->left));
		BinaryTreeDestory(&((*root)->right));
		free(*root);
		*root = NULL;
	}
}

4. 二叉树的结点个数

利用递归即可解决,整棵树的结点数 = 左子树的结点数 + 根结点(1个)+ 右子树的结点数

同理,左右子树也是相同的逻辑。

某棵子树(某结点的左子树或右子树)为空时,返回0。

// 二叉树节点个数
int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
		return 0;

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

5. 二叉树的叶子结点数

与上面的算法相似,但是此处我们只统计叶子结点数。

叶子结点区别于其他结点的特点是,其左右孩子都为空,如果当前结点符合条件我们就返回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);
}

6. 二叉树第k层的结点数

同样的递归思路,第k层结点数 = 左子树第k-1层结点数 + 右子树第k-1层结点数

但是我们需要一个参数k来判断当前结点位于第几层,以及是否需要返回。

每深入一层,k就减1,当k等于1时返回1,表示已找到一个所求结点。

// 二叉树第k层节点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;

	return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

7. 查找值为x的结点

如果当前结点的值为x,则直接返回当前结点。

否则,先到左子树进行寻找,若左子树没有(返回值为NULL)就找右子树。

// 二叉树查找值为x的节点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;

	BTNode* Find = BinaryTreeFind(root->left, x);
	if (Find == NULL)
	{
		Find = BinaryTreeFind(root->right, x);
	}
	return Find;
}

8.二叉树的递归与非递归遍历

二叉树的递归遍历较为简单,但是非递归算法却十分复杂,需要用到栈来模拟递归进行实现。

在该文章中有详细介绍:栈与递归的实现-CSDN博客

9. 树的高度

树的高度指树中结点的最大层次。

所以树的高度 = 左子树与右子树中较高者的高度 + 1(根结点)。

//树的高度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;

	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);
	return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}

 10. 层序遍历

层序遍历也就是广度优先遍历,实现这个算法需要用到队列的帮助。

由于C语言不支持队列,所以在这里写了一个简易的队列。

其中,Get函数会在获取队头结点的同时将其出队,这样实现对于我们当前算法的实现来说,使用起来更加方便。

当然,下面这段代码只是帮助理解,直接忽略也可,只要你知道我对队列进行了什么操作。

typedef BTNode* QDataType;

// 链式结构:表示队列 
typedef struct QListNode
{
	struct QListNode* _next;
	QDataType _data;
}QNode;

// 队列的结构 
typedef struct Queue
{
	QNode* _front;
	QNode* _rear;
	int size;
}Queue;

//初始化
void Inite(Queue* q)
{
	q->size = 0;
	q->_front = NULL;
	q->_rear = NULL;
}

//入队
void Push(Queue* q, QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->_data = x;
	newnode->_next = NULL;

	if (q->_rear == NULL)
	{
		q->_front = q->_rear = newnode;
	}
	else
	{
		q->_rear->_next = newnode;
		q->_rear = newnode;
	}
	q->size++;
}

//出队
QDataType Get(Queue* q)
{
	if (q->size == 0)
		return NULL;
	QDataType temp = q->_front->_data;
	QNode* del = q->_front;
	if (q->size == 1)
	{
		q->_rear = NULL;
	}
	q->_front = q->_front->_next;
	free(del);
	q->size--;

	return temp;
}

//销毁队列
void DestroyQueue(Queue* q)
{
	QDataType temp;
	while (q->_front)
	{
		temp = Get(q);
	}
}

基本思路就是:

1. 出队得到一个结点并访问。

2. 如果当前结点有孩子,则将其孩子全部入队。

3. 不断重复以上两步,知道队中无结点。

// 层序遍历
void BinaryTreeLevelOrder(BTNode* root)
{
	Queue q;
	Inite(&q);
	Push(&q, root);
	BTNode* cur = NULL;
	while (cur = Get(&q))
	{
		printf("%c ", cur->data);
		if(cur->left != NULL)
		Push(&q, cur->left);
		if(cur->right != NULL)
		Push(&q, cur->right);
	}
	DestroyQueue(&q);
}

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

根据完全二叉树的特点,我们总结出以下判断标准:

1. 如果某个结点只有右孩子而没有左孩子,则该树一定不是完全二叉树。

2. 如果某个结点不是满的(缺少左孩子或右孩子),则位置序号在其之后的结点一定既没有      左孩子,又没有右孩子,否则就是非完全二叉树。

// 判断二叉树是否是完全二叉树
int BinaryTreeComplete(BTNode* root)
{
	Queue q;
	Inite(&q);
	Push(&q, root);
	BTNode* cur = NULL;
	do
	{
		cur = Get(&q);
		if (cur->left == NULL && cur->right != NULL)
			return 0;
		else if(cur->left != NULL && cur->right != NULL)
		{
			Push(&q, cur->left);
			Push(&q, cur->right);
		}
	} while (cur->right);

	while (q.size != 0)
	{
		cur = Get(&q);
		if (cur->left != NULL || cur->right != NULL)
			return 0;
	}
	return 1;

	DestroyQueue(&q);
}

12. oj练习

12.1 单值二叉树. - 力扣(LeetCode)

第一种思路:保存根结点的值,然后用其与所有结点进行比较。

bool judge(struct TreeNode* root, int com)
{
    if(root == NULL)
    return true;
    if(root->val != com)
    return false;

    return judge(root->left, com) && judge(root->right, com);
}

bool isUnivalTree(struct TreeNode* root) {
    int com = root->val;
    return judge(root, com);
}

为了提高代算法的效率,也可以将com变量改为全局变量。

第二种思路:我们找到题目要求的一个等价条件——除了叶子结点,所有结点的值都与自己孩子的值相等。

bool isUnivalTree(struct TreeNode* root) {
    if(root == NULL)
    return true;
    
    if(root->left && root->left->val != root->val || root->right && root->right->val != root->val)
    return false;

    return isUnivalTree(root->left) && isUnivalTree(root->right);
}

12.2 相同的树. - 力扣(LeetCode)

采用递归的思路,两棵树相同意味着他们:根结点相同,左子树相同,右子树相同。

左右子树相同,包括左右子树都为空的情况。

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p == NULL && q == NULL)
    return true;
    if(p == NULL || q == NULL || p->val != q->val)
    return false;

    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

12.3 对称的树. - 力扣(LeetCode)

 

观察示例可以发现,这道题可以采用与上道题如出一辙的思路,只不过我们要比较的两棵树是根结点的左右子树。

同时,比较的逻辑也需要更改,左子树的右孩子应该与右子树的左孩子比较,左子树的左孩子应该与右孩子的右子树比较。

bool judge(struct TreeNode* root1, struct TreeNode* root2)
{
    if(root1 == NULL && root2 == NULL)
    return true;
    if(root1 == NULL || root2 == NULL || root1->val != root2->val)
    return false;

    return judge(root1->left, root2->right) && judge(root1->right, root2->left);
}

bool isSymmetric(struct TreeNode* root) {
    return judge(root->left, root->right);
}

12.4 另一棵树的子树. - 力扣(LeetCode)

这道题可以利用到“相同的树”那道题的函数,我们只需要遍历root,然后将其的子树依次与subRoot进行比较即可。

bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    if(p == NULL && q == NULL)
    return true;
    if(p == NULL || q == NULL || p->val != q->val)
    return false;

    return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}

bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot){
    if(root == NULL)
    return false;
    if(isSameTree(root, subRoot))
    return true;

    return isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot);
}

12.5 二叉树的前序遍历. - 力扣(LeetCode)

 这道题并不是简单地对二叉树进行前序遍历,而是要求我们返回存储前序遍历序列的数组。

首先需要动态开辟一个存储结果的数组,但问题是开辟多少合适呢?

当然,在这道题中我们可以直接开辟100个空间的数组。

假如我们不知道这么一个条件呢?

可以用动态的顺序表来管理,但鉴于需要我们自己实现,似乎就有点麻烦。

这时,我们就可以用到上面的BinaryTreeSize函数了,先求出树中的结点个数,再进行我们接下来的操作。

与利用前序遍历数组构建二叉树相同,我们这里也需要一个pi来指示数组当前的下标。

下面的这段代码中,直接用returnSize来充当了这个pi。

int TreeSize(struct TreeNode* root)
{
    return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

void pretraversal(struct TreeNode* root, int* arr, int* pi)
{
    if(root == NULL)
    return;

    arr[*pi] = root->val;
    (*pi)++;
    pretraversal(root->left, arr, pi);
    pretraversal(root->right, arr, pi);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    int* arr = (int*)malloc(sizeof(arr) * TreeSize(root));
    *returnSize = 0;
    pretraversal(root, arr, returnSize);
    return arr;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/633004.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

《微服务王国的守护者:Spring Cloud Dubbo的奇幻冒险》

5. 经典问题与解决方案 5.3 服务追踪与链路监控 在微服务架构的广袤宇宙中,服务间的调用关系错综复杂,如同一张庞大的星系网络。当一个请求穿越这个星系,经过多个服务节点时,如何追踪它的路径,如何监控整个链路的健康…

k8s遇到的错误记录

时隔四年有开始重新鼓捣k8s了,重新安装后遇到的错误记录如下: Error: Package: kubelet-1.14.0-0.x86_64 (kubernetes) Requires: kubernetes-cni 0.7.5 Available: kubernetes-cni-0.3.0.1-0.07a8a2.x86_64 (kubernetes) …

MySQL——存储过程,触发器

BaiduComate: # 问题1: # 问题1: 帮我创建两个表student与score表,要求student表有id,createDate,userName,phone,age,sex,introduce, 要求score表有id&…

全面掌握深度学习:从基础到前沿

引言:深入探索深度学习的世界 在人工智能(AI)的广阔领域中,深度学习已经成为最令人瞩目的技术之一。它不仅推动了科技的许多突破性进展,也正在改变我们的工作和生活方式。本博客旨在全面总结深度学习的关键知识点&…

Kubeblocks系列2-redis尝试之出师未捷身先死

背景: 上一节,完成了Kubeblocks系列1-安装。现在就想拿一个简单的应用测试一下kubeblocks这个所谓的神器是否好用,是否可以应用与生产! Kubeblocks系列2-redis尝试 参照官方文档:创建并连接到 Redis 集群 确保 Red…

使用docker+jenkins构建前端项目发布到nginx

1.准备环境 为了方便公司开发优化代码,不需要反复地将项目包发送给运维部署,我们对开发环境的前端项目利用jenkinsCI/CD进行自动化部署 需要两台服务器 一台jenkins 一台发布服务器,这里发布服务器 我直接使用开发环境的服务器 将admin界面与云计算展示…

python调用百度文心一言对话模型

近日,百度宣布其两款主力模型 ENIRE Speed、ENIRE Lite 可以免费使用。试了一下怎么程序调用。 1.准备工作 需要注册百度智能云账号,也可以使用原来的百度账号登录,登录之后要完成实名认证,才能使用API调用。在千帆大模型操作台…

5.23-

回顾 I0多路复用的原理? 程序首先向操作系统发起一个IO多路复用请求,告诉操作系统需要监视哪些IO通道。这些IO通道可以包括网络套接字、文件描述符等操作系统随后会将这些IO通道放入一个队列中,并在某个IO通道就绪时(如数据到达、文件可读…

滴滴三面 | Go后端研发

狠狠的被鞭打了快两个小时… 注意我写的题解不一定是对的,如果你认为有其他答案欢迎评论区留言 bg:23届 211本 社招 1. 自我介绍 2. 讲一个项目的点,因为用到了中间件平台的数据同步,于是开始鞭打数据同步。。 3. 如果同步的时候…

Linux-centos下安装ffmpeg的详细教程

源安装 第一种方式: . 首先需要安装yum源: 这个源安装的ffmpeg版本是3.4 yum install epel-release yum install -y https://mirrors.ustc.edu.cn/rpmfusion/free/el/rpmfusion-free-release-7.noarch.rpm然后可以安装ffmpeg yum install -y ffmpeg ff…

data studio连接到虚拟机上的openGauss

参考:使用DataStudio连接本地虚拟机中的opengauss数据库_big data_白日梦想家_胖七七-华为云开发者联盟 本实验虚拟机安装的是CentOS7 数据库版本是:openGauss-5.0.2-CentOS-64bit-all.tar.gz 1.配置pg_hba.conf 首先使用su - omm登录到omm用户&…

家电维修上门维修小程序怎么搭建制作?

​在家庭生活中,家电的维修问题一直是人们关注的焦点。随着微信小程序的普及,家电维修服务行业也迎来了线上转型的机遇。一款便捷、高效的家电维修上门维修小程序,不仅能为维修服务商带来新的客户,也能为用户带来更便捷的服务体验…

JavaWeb基础(HTML,CSS,JS)

这些知识用了三四天左右学完,因为是JavaWeb,并不是前端,所以只是够用,不是深入,但是这确实是学校一个学期交的东西(JavaWeb课程)。 总结一下网页分为三部分:HTML(内容结构),CSS&…

详解ArcGIS 水文分析模型构建

目录 前言 项目环境、条件 Dem 数据预览 ArcGIS模型构建器 模型搭建 填洼 流向 流量 河流长度 栅格计算器 河流链接 河网分级 栅格河网矢量化 绘制倾泻点 栅格流域提取 集水区 盆域分析 栅格转面 模型应用 导出 py 文件 完善脚本 最终效果 结束语 前言 …

网络安全技术心得体会

网络与信息安全技术心得体会 通过对网络安全这门课程的学习,我进一步了解了网络安全技术的相关知识。大致来说,所谓网络安全指的是对网络系统中各类软硬件和数据信息等提供保护屏障,确保数据信息不受到恶意侵入、窃取等破坏,保证…

modelbox验证expand和condition共用后,是否顺序保持

如图,在expand之后接了个condition,上下两个流中每一对数据buffer的顺序性是否还会保持? 笔者修改让condition在遇到奇数和偶数时的走向不同。 然后在response单元输出每一对数据,发现顺序都不变。且在处理时,输出会卡…

【C语言深度解剖】(16):C语言的文件读写操作

🤡博客主页:醉竺 🥰本文专栏:《C语言深度解剖》 😻欢迎关注:感谢大家的点赞评论关注,祝您学有所成! ✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看&…

微服务框架Go-kit 01 - 基础示例

一、Go kit简介 Go kit 是一个用于构建可扩展、灵活和可维护微服务的框架和工具集合。它提供了一系列库和组件,涵盖了微服务开发的各个方面,包括服务发现、负载均衡、通信、日志记录、请求跟踪、限流、熔断等。 Go kit 构建微服务时遵循一种类似于传统…

成都爱尔胡建斌院长提醒近视超过600度,记得每年检查眼底!

高度近视是指近视度数在600度及以上的一种屈光不正的状态。 近视的眼睛必定是变形的。在正常情况下,人的眼球类似球体,但随着近视加深,眼轴变长,眼球体积逐渐增大,整个眼球从圆球型向椭圆球形发展,而眼球壁…

HTTPS 协议原理详解

HTTPS 协议原理详解 什么是 HTTPS 协议什么是 SSL/TSL 层HTTPS 使用到的加密算法HTTPS 中 TLS 层的加密过程详解HTTPS 加密过程中用到的数字证书 什么是 HTTPS 协议 HTTPS (全称:Hypertext Transfer Protocol Secure ),是以安全为…