c语言实现二叉树(链式结构)

news2024/12/22 20:01:12

文章目录

  • 前言
  • 一、二叉树的遍历
    • 1、二叉树的层序遍历
    • 2、二叉树的前序遍历
    • 3、二叉树的中序遍历
    • 4、二叉树的后序遍历
    • 5、代码实现
  • 二、二叉树的一些操作的实现
    • 1、求二叉树的结点个数
    • 2、求二叉树叶子结点个数
    • 3、求二叉树第k层结点个数
    • 4、求二叉树深度
    • 5、二叉树中查找值为x的结点
    • 6、二叉树的销毁
  • 三、二叉树层序遍历的实现
    • 1、层序遍历
    • 2、层序遍历代码实现
    • 3、层序遍历应用 -- 判断二叉树是否是完全二叉树


前言

当我们使用顺序结构实现了二叉树的存储后,接下来就是使用链式结构来实现二叉树的存储。

一、二叉树的遍历

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

1、二叉树的层序遍历

二叉树的层序遍历在前面介绍二叉树的顺序结构存储时,已经使用到了,即完全二叉树在数组中的存储顺序就是该二叉树的层序遍历顺序。
在这里插入图片描述

2、二叉树的前序遍历

前序遍历(Preorder Traversal 亦称先序遍历)——访问根结点的操作发生在遍历其左右子树之前。#号表示空树的位置。
在这里插入图片描述

3、二叉树的中序遍历

中序遍历(Inorder Traversal)——访问根结点的操作发生在遍历其左右子树之中(间)。#号表示空树的位置。

在这里插入图片描述

4、二叉树的后序遍历

后序遍历(Postorder Traversal)——访问根结点的操作发生在遍历其左右子树之后。#号表示空树的位置。
在这里插入图片描述

5、代码实现

知道了二叉树的前、中、后序遍历的规则后,我们就可以实现一个二叉树,然后实现这些遍历方法。
二叉树定义

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

typedef int BTDataType;

typedef struct BinaryTreeNode
{
	//存储该结点的左子树的根结点地址
	struct BinaryTreeNode* left;
	//存储该结点的右子树的根结点地址
	struct BinaryTreeNode* right;
	//存储该结点的数据
	BTDataType data;
}BTNode;

创建二叉树结点

//创建一个新的结点,并且将该结点返回
BTNode* BuyNode(BTDataType x)
{
	BTNode* newNode = (BTNode*)malloc(sizeof(BTNode));
	if (newNode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	newNode->left = NULL;
	newNode->right = NULL;
	newNode->data = x;
	return newNode;
}

创建一棵二叉树

//创建一棵二叉树,并且返回该二叉树的根结点
BTNode* CreateBinaryTree()
{
	//建立二叉树的结点
	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;
}

实现先序遍历

//先序遍历
void PreOrder(BTNode* root)
{
	//如果访问的结点为NULL,就打印#
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	//如果访问结点不为NULL,就将该结点的数据打印出来
	printf("%d ", root->data);
	//因为为先序遍历,所以先访问根节点,然后再访问左子树
	PreOrder(root->left);
	//当左子树访问完再访问右子树
	PreOrder(root->right);
}

即函数递归调用的顺序如图。
在这里插入图片描述

实现中序遍历

//中序遍历
void PreOrder(BTNode* root)
{
	//如果访问的结点为NULL,就打印#
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	//因为为中序遍历,所以先访问左子树,然后再访问根节点
	PreOrder(root->left);
	//左子树访问完后再打印根结点数据
	printf("%d ", root->data);
	//当根结点访问完再访问右子树
	PreOrder(root->right);
}

实现后序遍历

//后序遍历
void PostOrder(BTNode* root)
{
	//如果访问的结点为NULL,就打印#
	if (root == NULL)
	{
		printf("# ");
		return;
	}
	//因为为后序遍历,所以先访问左子树,然后再访问右子树
	PostOrder(root->left);
	//当左子树结点访问完毕后,访问右子树结点
	PostOrder(root->right);
	//当左右子树结点都访问完后,再访问根节点数据
	printf("%d ", root->data);
}

二、二叉树的一些操作的实现

当我们实现了二叉树的遍历后,就可以再实现二叉树的其他一些操作

1、求二叉树的结点个数

当要求二叉树结点时,我们想到的第一个方法就是定义一个全局变量count,然后再依次访问二叉树的结点,如果结点不为空,则count++,最后count的值就是二叉树中的结点个数。需要注意的是,当我们第二次调用这个方法时,因为count为全局变量,此时已经不为0,所以要重置count的值为0。

int count = 0;
void BinaryTreeSize(BTNode* root)
{
	//如果访问的结点为NULL,则count不进行+1
	if (root == NULL)
	{
		return;
	}
	//每访问到一个不为NULL结点就让count+1
	++count;
	//然后再去访问该结点的左子树和右子树
	BinaryTreeSize(root->left);
	BinaryTreeSize(root->right);
}

上述方法使用全局变量得到树节点的数量是不安全的,并且全局变量可以被修改,而且每次调用函数时,还需要重置全局变量。所以我们可以使用第二种方法来求出二叉树结点个数。

int TreeSize2(BTNode* root)
{
	//当root结点为NULL时,就返回0
	if (root == NULL)
	{
		return 0;
	}
	//当root结点不为NULL,就返回root结点的数量,然后加上root结点左子树和右子树的结点的数量。
	return 1 + TreeSize2(root->left) + TreeSize2(root->right);
}

函数的递归和回退图。
在这里插入图片描述

2、求二叉树叶子结点个数

//二叉树叶子结点个数
int BinaryTreeLeafSize(BTNode* root)
{
	//如果该结点为NULL,则返回0
	if (root == NULL)
	{
		return 0;
	}
	//如果该结点为叶子结点,则返回1
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	//如果该结点不为NULL,也不为叶子结点,就返回该节点的左子树和右子树中的叶子结点数
	return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

3、求二叉树第k层结点个数

//二叉树第k层结点个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{
	assert(k >= 1);
	//如果该结点为NULL,就返回0
	if (root == NULL)
	{
		return 0;
	}
	//k==1,说明要求的就是这一层的结点数,返回1
	if (k == 1)
	{
		return 1;
	}
	//如果该结点不为NULL,且k!=1,说明求的不是该层的结点数,让k-1,然后求该结点的左子树和右子树
	return BinaryTreeLevelKSize(root->left,k-1) + BinaryTreeLevelKSize(root->right,k-1);
}

4、求二叉树深度

//求二叉树深度
int BinaryTreeDepth(BTNode* root)
{
	//如果该结点为NULL,则深度为0
	if (root == NULL)
	{
		return 0;
	}
	//然后求出该结点的左子树和右子树的深度
	int leftDepth = BinaryTreeDepth(root->left);
	int rightDepth = BinaryTreeDepth(root->right);
	//如果该结点的左子树深度大于右子树深度
	if (leftDepth > rightDepth)
	{
		//就返回该结点左子树的深度加这一层的深度
		return leftDepth + 1;
	}
	//如果该结点的左子树深度小于等于右子树深度
	else
	{
		//就返回右子树的深度加这一层的深度
		return rightDepth + 1;
	}
}

5、二叉树中查找值为x的结点

//二叉树查找值为x的结点
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	//当结点为NULL时,返回NULL
	if (root == NULL)
	{
		return NULL;
	}
	//当该结点数据为x时,返回该结点
	if (root->data == x)
	{
		return root;
	}
	//当该结点数据不为x时,先遍历该结点的左子树
	BTNode* left = BinaryTreeFind(root->left, x);
	//如果该结点的左子树返回的结点不为NULL,则说明在左子树中找到了存储x的结点,则此时left就存储该结点的地址。直接将left返回即可
	if (left!=NULL)
	{
		return left;
	}

	//如果该结点的左子树也没有查到就去遍历该结点的右子树,
	BTNode* right = BinaryTreeFind(root->right, x);
	//当该结点的右子树返回的结点不为NULL,则说明在右子树中找到了存储x的结点,此时right就存储该结点的地址。直接将right返回即可
	if (right!=NULL)
	{
		return right;
	}
	
	//当该结点的数据和该结点的左子树和右子树的结点中都没有该数据,则二叉树中没有该数据,此时返回NULL
	return NULL;
}

6、二叉树的销毁

二叉树的销毁就是依次将二叉树的各结点申请的空间都释放,则需要遍历一遍二叉树,这里我们需要采用后序遍历,即先将根结点的左子树的结点释放完,再释放根结点的右子树的结点,最后再释放根结点的空间。因为如果采用先序遍历先将根结点空间释放,则就找不到根节点的左子树和右子树了。

//二叉树的销毁
void BinaryTreeDestroy(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	//采用后序遍历释放二叉树结点
	BinaryTreeDestroy(root->left);
	BinaryTreeDestroy(root->right);
	free(root);
}

三、二叉树层序遍历的实现

1、层序遍历

二叉树的层序遍历的实现需要队列的辅助。例如有下面一棵二叉树,先将二叉树的根结点入队列。
在这里插入图片描述
然后将队列的队头出队,并且将队头结点的左右孩子入队列。
在这里插入图片描述
然后重复上述操作。将剩下的结点依次进入队列。
在这里插入图片描述
直到队列为空。结点出队的顺序即为二叉树层序遍历顺序。
在这里插入图片描述

2、层序遍历代码实现

层序遍历需要借助一个辅助队列,先将二叉树根结点入队,然后让根节点出队,让根节点的左右孩子入队,然后让左右孩子出队,让左右孩子的左右孩子入队,依次循环下去,直到队列为空。

//二叉树的层序遍历
void LevelOrder(BTNode* root)
{
	//创建一个辅助队列
	Queue qt;
	QueueInit(&qt);
	//如果二叉树为空,就退出函数
	if (root == NULL)
	{
		return;
	}
	//将根节点的地址入队
	QueuePush(&qt, root);
	//如果队列中不为空,就进入循环
	while (!QueueEmpty(&qt))
	{
		//创建一个BTNode*类型的指针接收队列中队头结点存的地址
		BTNode* head = QueueFront(&qt);
		//将队头结点出队。
		QueuePop(&qt);
		//打印出队结点的数据
		printf("%d ", head->data);
		//如果出队结点的左孩子不为空,就将结点左孩子入队
		if (head->left != NULL)
		{
			QueuePush(&qt, head->left);
		}
		//如果出队结点的右孩子不为空,就将结点右孩子入队
		if (head->right != NULL)
		{
			QueuePush(&qt, head->right);
		}
	}
	printf("\n");
	QueueDestroy(&qt);

}

3、层序遍历应用 – 判断二叉树是否是完全二叉树

当有一棵二叉树需要判断其是否为完全二叉树时,此时就可以借用二叉树的层序遍历。
例如有如下一个完全二叉树
在这里插入图片描述
该二叉树按照层序遍历的步骤,先让根结点入队,然后队头结点出队,并让队头结点存的结点的左右孩子入队。如果左右孩子为空也入队。
在这里插入图片描述
当队列中队头结点存的数据为NULL时,停止上述操作。此时队列中结点存储的数据全为NULL,可由此得出该二叉树为完全二叉树
在这里插入图片描述

例如有如下一个非完全二叉树
在这里插入图片描述
该二叉树按照层序遍历的步骤,先让根结点入队,然后队头结点出队,并让队头结点存的结点的左右孩子入队。如果左右孩子为空也入队。
在这里插入图片描述
当队列中队头结点存的数据为NULL时,停止上述操作。此时队列中的结点存储的数据有的为NULL,有的不为NULL,则可以由此得出该二叉树不为完全二叉树。
在这里插入图片描述
由上述的条件我们可以修改层序遍历的一些逻辑,使其可以判断二叉树是否为完全二叉树。

int BinaryTreeComplete(BTNode* root)
{
	//创建一个辅助队列
	Queue qt;
	QueueInit(&qt);
	
	//如果二叉树根节点不为空,就将根节点的地址入队列
	if (root != NULL)
	{
		QueuePush(&qt, root);
	}
	
	//如果队列中不为空,就进入循环
	while (!QueueEmpty(&qt))
	{
		//将队列的队头结点存的数据返回
		BTNode* head = QueueFront(&qt);
		//将队头结点出队
		QueuePop(&qt);
		//如果该队头结点存的数据不为NULL,就将该结点的左右孩子入队,左右孩子为NULL也入队
		if (head != NULL)
		{
			QueuePush(&qt, head->left);
			QueuePush(&qt, head->right);
		}
		//如果该队头结点存的数据为空,说明已经到达队列中第一个NULL,此时跳出循环
		else
		{
			break;
		}
	}
	//此时如果队列不为空,就继续遍历队列中的元素
	while (!QueueEmpty(&qt))
	{
		//将队头结点存的数据返回
		BTNode* head = QueueFront(&qt);
		//将队头结点出队
		QueuePop(&qt);
		//如果队列中第一个NULL后还有队列结点存的数据不为NULL,则说明该二叉树不为完全二叉树
		if (head != NULL)
		{
			//将队列销毁
			QueueDestroy(&qt);
			//返回0则代表该二叉树不是完全二叉树
			return 0;
		}
	}
	//如果当队列中结点全部遍历完,并且存的数据都为NULL,说明该二叉树为完全二叉树
	//销毁队列
	QueueDestroy(&qt);
	//返回1代表为完全二叉树
	return 1;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

彻底理解浏览器的缓存机制

前言 在前端性能优化的方式中&#xff0c;最重要的当然是缓存了&#xff0c;使用好了缓存&#xff0c;对项目有很大的帮助。比如我们访问网页时&#xff0c;使用网页后退功能&#xff0c;会发现加载的非常快&#xff0c;体验感很好&#xff0c;这就是缓存的力量。 什么是缓存…

SpringBoot的四种handler类型

Controller ReuestMapping 实现Controller接口 使用Component将该类封装成一个Bean 实现HttpRequestHandler 实现RouterFunction

upload-labs文件上传漏洞靶场练习

任意文件上传靶场upload-labs下载地址 文章目录 Pass-01- 前端JS校验绕过Pass-02- 文件类型MIME类型绕过Pass-03- 文件名后缀黑名单绕过Pass-04- .htaccess绕过Pass-05- 文件名后缀大写绕过Pass-06- 文件名后缀加空格绕过Pass-07- 文件名后缀加点绕过Pass-08-文件名后缀 ::$DAT…

控制goroutine 的并发执行数量

goroutine的数量上限是1048575吗&#xff1f; 正常项目&#xff0c;协程数量超过十万就需要引起重视。如果有上百万goroutine&#xff0c;一般是有问题的。 但并不是说协程数量的上限是100多w 1048575的来自类似如下的demo代码: package mainimport ( "fmt" "ma…

SpringMVC使用

文章目录 一.MVC基础概念1.MVC定义2.SpringMVC和MVC的关系 二.SpringMVC的使用1.RequestMapping2.获取参数1.获取单个参数2.传递对象3.后端参数重命名&#xff08;后端参数映射&#xff09;4.获取URL中参数PathVariable5.上传文件RequestPart6.获取Cookie/Session/header 3.返回…

电视盒子什么牌子好?经销商整理线下热销电视盒子品牌排行榜

在面对众多品牌和型号时&#xff0c;不知道电视盒子哪个牌子好的消费者超多&#xff0c;很多人进店都会问我电视盒子哪款好&#xff1f;我根据店铺内近两年的销量情况整理了电视盒子品牌排行榜&#xff0c;看看实体店哪些电视盒子最值得入手吧。 TOP 1.泰捷WEBOX 40Pro Max电视…

案例实操-获取员工数据

案例&#xff1a;获取员工数据&#xff0c;返回统一响应结果&#xff0c;在页面渲染展示 package com.bignyi.controller;import com.bignyi.pojo.Emp; import com.bignyi.pojo.Result; import com.bignyi.utils.XmlParserUtils; import org.springframework.web.bind.annotat…

分享一个在线二维码生成器(基于qrcode.js开发)

一种二维码扫描与生成的工具, 它可生成个性化二维码, 支持文本、网址、图片、短信、电话等格式及主题,提供融合码功能 演示地址 https://qrcode.gitapp.cn 关键代码 var qrcode new QRCode(document.getElementById("qrcode"), {text: "",width: 288,h…

2023下半年西安/北京/深圳NPDP产品经理国际认证开班啦

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

【JavaSE】Java快速入门

Java main 函数 public class Main {public static void main(String[] args) { System.out.printf("Hello and welcome!");} }与C命名规范不同&#xff0c;Java 的命名形式最好使用驼峰法 Java 注释 C/C常用的两种注释习惯Java都可以使用&#xff0c;Java自身…

【HASH值获取】

命令行输入&#xff1a;C:\Users\Administrator>certutil -hashfile SIC-1000.exe md5

2、[春秋云镜]CVE-2022-30887

文章目录 一、靶标介绍二、复现过程 一、靶标介绍 二、复现过程 &#xff08;1&#xff09;打开网址。 &#xff08;2&#xff09;查看源代码 邮件格式&#xff1a;第一个符号不准为&#xff0c;后续符号有、.&#xff1b; 密码格式&#xff1a;匹配所有小写字母&#xff0c…

数组 刷题常用

在写数组模拟常用到数组&#xff0c;借此把常用的记下来以便查阅 一维数组&#xff0c;若初始化为0&#xff0c;可以用int a[N] {0}或者int a[N]{}. 但是若是其他值&#xff0c;不可类似地初始化为int a[N] {0}&#xff0c;而应写成memset或者fill赋值的方法。 首先便是二维…

康希诺的再估值:市场到底,行业向上

生物医药是整个二级市场弹性数一数二&#xff0c;但拐点难以揣摩的行业。这一点&#xff0c;美港A三大市场都曾经有过足够多的暴涨暴跌案例可用于佐证。 但很多时候&#xff0c;这种片面的表现又掩盖了生物医药自身的永续价值&#xff1a;在绝大多数细分赛道上&#xff0c;任何…

激活Conda环境并在pycharm使用

第一步&#xff1a;打开Anaconda Prompt 第二步&#xff1a;查看当前存在的虚拟环境 conda env list 第三步&#xff1a;创建虚拟环境 conda create -n 环境名 pythonX.X.X 如果不清楚python版本&#xff0c;可以用以下命令查看&#xff1a; 第四步&#xff1a;激活指定虚拟环…

客户案例|MemFire Cloud助推应急管理业务,打造百万级数据可视化大屏

「导语」 硬石科技&#xff0c;成立于2018年&#xff0c;总部位于武汉&#xff0c;是一家专注于应急管理行业和物联感知预警算法模型的技术核心的物联网产品和解决方案提供商。硬石科技作为一家高新技术企业&#xff0c;持有6项发明专利&#xff0c;拥有100余项各类平台认证和资…

nginx+tomcat部署的项目,上传文件成功,但请求文件报403 forbidden

这是因为上传文件时tomcat创建的目录、文件&#xff0c;nginx没有权限访问导致。 最快的解决方法是修改$tomcat_home/bin/catalina.sh 修改之后记得重启tocmat 参考&#xff1a; https://www.cnblogs.com/mgds/p/16129039.html

解读未知:文本识别算法的突破与实际应用

解读未知&#xff1a;文本识别算法的突破与实际应用 1.文本识别算法理论 背景介绍 文本识别是OCR&#xff08;Optical Character Recognition&#xff09;的一个子任务&#xff0c;其任务为识别一个固定区域的的文本内容。在OCR的两阶段方法里&#xff0c;它接在文本检测后面…

攻防世界-Erik-Baleog-and-Olaf

原题 解题思路 文件名就说了stego&#xff0c;改成图片后缀再用stegslove打开。 扫码即可