二叉树简单OJ题(及其后续函数补充)

OJ题

单值二叉树

在这里插入图片描述

首先呢,我们还是把问题分化一下,求一棵二叉树是否为单值二叉树,还是可以分为几个部分:根节点 左子树 右子树
而我们向下遍历的时候,其实就是在这个节点以及其左子树和右子树中找,是否值都相同,这样一来,代码写出就十分明了了

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

现在我们就对这段代码进行分析:假如图长这个样子:
在这里插入图片描述
首先从1开始,1不满足上面两个条件,递归到左右子树;假设1的左子树为“2”,右子树为“3”,那么“2”也不满足上述两个if语句的条件,递归到它的左右子树;“2”的左右子树,一个为空,直接返回true,另一个在不满足两个if语句,继续向下递归时,两个子树都为空,返回true,故“2”的两个子树的返回值均为true,导致’2"返回给1的bool值也为true。而“3”的分析则是与“2”的左子树相同,左子树右子树均为空,全部返回true,导致“3”返回给1的值也为true,最终1的两个返回值都为true,导致返回到外部main函数的值也为true

相同的树

跟上述题目一样,分析二叉树的OJ题,实际上就是要把递归的返回值和情况弄清楚

  1. 两个树的节点都为空
  2. 两个树的节点一个为空,一个不为空
  3. 两个树的节点都不为空,但是不相等
bool isSameTree(struct TreeNode* p, struct TreeNode* q) {
    //都为空
    if (!p && !q)
        return true;

    //一个为空另一个不为空
    if (!p || !q)
        return false;

    if (p && q && p->val != q->val)
        return false;

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

用一个简单的例子来说明一下
在这里插入图片描述
假如有这样一个二叉树,有两棵,那么首先遍历1,不为空且相等,向下递归到2和3;2的情况与1相同,所以继续向下递归,都为空,所以来返回真,而2返回给1的值也是真;而3与2的情况相同,是一样的递归过程

二叉树的先序遍历


这道题的难点不在于先序遍历,而是在于如何把先序遍历的结果放进一个数组并返回

//首先要找到节点的个数,为创建数组做准备
int TreeSize(struct TreeNode* root) {
    return root == NULL ? 0 : TreeSize(root->left) + TreeSize(root->right) + 1;
}

//核心功能
void preorder(struct TreeNode* root, int* a, int* pi) {
    if (root == NULL)
        return;

    a[(*pi)++] = root->val; //把值放进数组里
    preorder(root->left, a, pi);
    preorder(root->right, a, pi);
}

int* preorderTraversal(struct TreeNode* root, int* returnSize) {
    int n = TreeSize(root); //先算出节点个数
    int* a = (int*)malloc(sizeof(int) * n);
    *returnSize = n; //因为它不知道这个数组有多大,要返回给系统看

    int i = 0;
    preorder(root, a, &i);
    return a;
}

有同学肯定会疑惑为什么实现核心功能的函数要传递一个int*pi,其实这是有原因的,如果我们传入的是int pi,那么在出现一个节点有两个子树的时候,由于pi只是一个形参,所以在左子树使用了之后,右子树使用pi的时候,实际上pi并没有++,导致左子树和右子树遍历时所赋的值实际上都在一个地址上,从而出现错误。而用指针作为参数,就可以改变实参,从而不会出现这种状况

我们还有另外一种方法,那就是使用全局变量,但是其存在多线程问题,如果同时使用,即使在使用函数前置过0,也可能出现线程安全问题,所以不建议使用。

对称二叉树

在这里插入图片描述
其实这个就比较像两个相等二叉树的翻版,只不过,它是左子树的左节点跟右子树右节点相等,而左子树的右节点跟右子树的左节点相等

bool _isSymmetric(struct TreeNode* root1, struct TreeNode* root2) {
    if (root1 == NULL && root2 == NULL)
        return true;//两边都是空,就是相等的

    if (!root1 || !root2)
        return false;//有一个是空的,肯定不相等

    if (root1->val != root2->val)
        return false;//不为空但值不相等,也不可以

    return _isSymmetric(root1->left, root2->right) &&
           _isSymmetric(root1->right, root2->left);//其实就是上述写的原理
}

bool isSymmetric(struct TreeNode* root) { 
    return _isSymmetric(root->left, root->right); //根节点不用比,从根节点的左子树和右子树开始比
    }

另一棵树的子树

在这里插入图片描述

我们首先需要弄清题目的意思,其实就是在一个较大的二叉树中找到较小二叉树的部分,在没找到相同的部分之前,都是向下遍历,而找到之后,就开始比较这个整体是否相同,如果相同,就返回true,不然就继续向下遍历。
先给大家看一段错误代码

bool isSubTree(struct TreeNode* root,struct TreeNode* subRoot){
	if(root==NULL)
		return false;
	
	if(root->val==subRoot->val)
		return isSameTree(root->subRoot);
	
	return isSubTree(root->left,subRoot)
		||isSubTree(root->right,subRoot);
}

这段代码乍一看上去逻辑也没问题,但是给一个例子给大家看就懂了
在这里插入图片描述
假设subRoot是4 1 2,那么首先从根节点3开始遍历,不满足条件,所以向下递归;左子树是4,满足条件,进入isSameTree函数:4的左子树4与1不相等,所以返回false,于是isSubTree(root->left,subRoot)返回的是false,而右子树5不满足条件,向下递归,左右子树都为空,返回false,导致5返回给3的结果也是false,两个false,导致最后isSubTree函数返回的就是false,显示的结果是没找到,但其实4 1 2就在下面,但是它在找到一个根节点一样的之后,只要后面不是,就会直接返回false,这样子就只能比较一个子树,而即使后面有也不会继续遍历,是错误的。
正确的应当是这样的:

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

    if (root->val == subRoot->val) {
        if (isSameTree(root, subRoot))
            return true;
    }

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

这样子的话,当两个节点的值相等,它们会向下递归,看是否完全相同,如果不是完全相同,也不会返回false,而是继续向下遍历,就不会出现上述错误。

除了这个方式,还有一个更简洁的代码:

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

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

一开始看可能会有点懵,但是其实就是要记住||符号的意义。首先诊断是否为空这个还是不变,然后如果值相等,isSameTree就来判断其是否左子树右子树也相等,如果为真就不用往下面继续遍历;如果值不相等,就会向左子树和右子树遍历,左子树找到了,就不用看右子树,左子树没找到就继续看右子树,其实逻辑理顺了之后还是比较清晰的。

函数补充

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

首先我们给一个例子,“ABD##E#H##CF##G##”,它构建出来的二叉树是这个样子的
在这里插入图片描述
我们讲解一下原理,首先直接的符号意思就是构建一个二叉树节点,而#意思就是这个节点没有,所以ABD的意思就是由A为根节点延伸出一条左子树,而后面的##则是指D没有左右子树,于是回到D,E表示其为D的右子树,而#H则表示E没有左子树,但是有右子树H。##表示H没有左右子树,返回到A,CF表示A的右子树为C,C的左子树为F,##G##表示F没有左右子树,C的右子树为G,且G没有左右子树,再次回到A,递归结束。

代码如下:

TreeNode* TreeCreate(char* a, int* pi)//a是给定数组,pi的意思是为了防止传入的是形参,在后续的递归中会导致当一个节点有两棵子树在同一层次时,pi值因为无法改变而相同,造成下标错误
{
	if (a[*pi] == '#')
	{
		(*pi)++;
		return NULL;
	}

	TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[(*pi)++];//数组值传入节点,下标++

	root->left_child = TreeCreate(a, pi);
	root->right_child = TreeCreate(a, pi);//两次递归,每次递归*pi值是不同的
	return root;//返回根节点
}

二叉树的销毁

这个比较简单,直接上代码,不过过程需要解释一下为什么使用后序遍历而不是前序或者中序

void DestroyTree(TreeNode* root)
{
	if (root == NULL)
		return;//为空就不需要销毁

	DestroyTree(root->left_child);
	DestroyTree(root->right_child);
	free(root);
}

在这里插入图片描述
以这个图为例,从根节点A开始遍历,递归到B和C两棵子树,B递归到D和E两棵子树,D和E的两棵子树都为空,直接返回到D和E节点,于是D和E被free掉,之后返回到B,B也被free掉,然后右子树跟左子树是相同的,也是FGC顺序被free掉,最后freeA

在二叉树中,节点之间存在父子关系。如果先销毁父节点,那么子节点的销毁可能会受到影响,因为子节点可能还依赖于父节点。而后序遍历的顺序是先遍历子节点,再遍历父节点,这样就可以确保在销毁父节点之前,其子节点已经被销毁。

层次遍历

还是先给大家展开一张图:
在这里插入图片描述
大家都知道,层次遍历就是每个层级从左到右遍历的过程,比如这里层次遍历的结果就是1 24 356,那么要怎样才能做到按照每个层级每个层级的顺序输出来呢,这就要用到我们之前学过的队列啦,我们可以在让根节点进入之后,输出根节点之后Pop根节点,然后再让根节点的左右子树分别Push进去,这样子,我们就可以写出一个初具规模的层次遍历函数:

void LevelOrder(TreeNode* root)
{
	Queue q;
	QueueInit(&q);//创建队列
	if (root)
		QueuePush(&q, root);//根节点不为空,节点进入

	while (!QueueEmpty(&q))//循环判断条件,队列不为空
	{
			TreeNode* front = QueueFront(&q);//front记录队头元素
			QueuePop(&q);//队头元素出队

			printf("%d ", front->data);//打印队头元素的值

			if (front->left_child)
				QueuePush(&q, front->left_child);//有左子树,左子树push进去

			if (front->right_child)
				QueuePush(&q, front->right_child);//同理
		}
		printf("\n");
	QueueDestroy(&q);//最后销毁队列
}

按照上述代码的逻辑,遍历过程如下:

将根节点1加入队列。
打印节点1的值。
将节点2和节点4加入队列(因为它们是节点1的子节点)。
打印节点2的值。
将节点3加入队列(因为它是节点2的左子节点)。
打印节点4的值。
将节点5加入队列(因为它是节点4的左子节点)。
将节点6加入队列(因为它是节点4的右子节点)。
当队列为空时,遍历结束。

当一个层次的节点被出完后,队头就变成了下一层次的最左边的节点的地址,这也是一个重点,就是我们传输的不是节点,而是节点的地址,否则我们无法通过节点找到它的子节点并继续向下遍历

当然,这只是实现了最基础的功能,我们还并没有完全实现其按照层次划分并输出的功能,这时候就引出下面的一步:利用变量levelSize和QueueSize,得以划分层次,并且每层节点个数也并未出错

void LevelOrder(TreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	int levelSize = 1;//初始根节点就1个
	while (!QueueEmpty(&q))
	{
		// 一层一层出
		while (levelSize--)//n--循环n次
		{
			TreeNode* front = QueueFront(&q);
			QueuePop(&q);

			printf("%d ", front->data);

			if (front->left_child)
				QueuePush(&q, front->left_child);

			if (front->right_child)
				QueuePush(&q, front->right_child);
		}
		printf("\n");

		levelSize = QueueSize(&q);//比如1的左右节点进去,队列中元素个数2个,故下一次levelSize=2,这样子的
	}
	printf("\n");

	QueueDestroy(&q);
}

是否为完全二叉树

大概原理是这样子的,其实就是从层次遍历修改而来。首先我们还是先把根节点push进去,然后开始遍历。但当遍历到空时,这个时候就跳出第一个循环,并看后续是否有非空节点,如果有,那就返回false,如果没有,那就最终返回true

//判断是否为完全二叉树
bool TreeComplete(TreeNode* root) {
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);

	int levelSize = 1;
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front == NULL)
			break;

		QueuePush(&q, front->left_child);
		QueuePush(&q, front->right_child);
	}

	// 前面遇到空以后,后面还有非空就不是完全二叉树
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)
		{
			QueueDestroy(&q);
			return false;
		}
	}

	QueueDestroy(&q);
	return true;
}

其实二叉树的知识还有很多复杂的,但是在C语言阶段很难去简单的实现,所以就让我们在C++的文章中再见吧!

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

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

相关文章

本地git切换地区后,无法使用ssh访问github 22端口解决方案

问题 由于放假回家,发现之前一直使用正常的git,与github无法通讯,pull和push都无法连接。报错如下: connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository. 原因 可能是所…

[学习笔记]刘知远团队大模型技术与交叉应用L3-Transformer_and_PLMs

RNN存在信息瓶颈的问题。 注意力机制的核心就是在decoder的每一步,都把encoder的所有向量提供给decoder模型。 具体的例子 先获得encoder隐向量的一个注意力分数。 注意力机制的各种变体 一:直接点积 二:中间乘以一个矩阵 三:…

常见Linux 命令,可以解决日常99%的问题

1、基本命令 uname -m 显示机器的处理器架构uname -r 显示正在使用的内核版本dmidecode -q 显示硬件系统部件(SMBIOS / DMI) hdparm -i /dev/hda 罗列一个磁盘的架构特性hdparm -tT /dev/sda 在磁盘上执行测试性读取操作系统信息arch 显示机器的处理器架构uname -m 显示机器的处…

【代码编辑器】VS Code安装下载教程及后续基本配置(一)[小白看这一篇足够了,手把手教会]

前言 Visual Studio Code(简称“VS Code” )是Microsoft在2015年4月30日[Build]开发者大会上正式宣布一个运行于 [Mac OS X]、[Windows]和[ Linux]之上的,针对于编写现代[Web]和[云应用]的跨平台[源代码编辑器],可在桌面上运行,并…

打折:阿里云国外服务器价格购买优惠活动

阿里云国外服务器优惠活动「全球云服务器精选特惠」,国外服务器租用价格24元一个月起,免备案适合搭建网站,部署独立站等业务场景,阿里云服务器网aliyunfuwuqi.com分享阿里云国外服务器优惠活动: 全球云服务器精选特惠…

vectorCast添加边界值分析测试用例

1.1创建项目成功后会自动生成封装好的函数,在这些封装好的函数上点击右键,添加边界值分析测试用例,如下图所示。 1.2生成的用例模版是不可以直接运行的,需要我们分别点击它们,让它们自动生成相应测试用例。如下图所示,分别为变化前和变化后。 1.3点击选中生成的测试用例,…

【轮式平衡机器人】——角度/速度/方向控制分析软件控制框架

轮式平衡机器人具有自不稳定性,可类比一级倒立摆系统的控制方法,常见有反馈线性化方法、非线性PID控制、自适应控制、自抗扰控制,还有改进的传统缺乏对外界干扰和参数改变鲁棒性的滑模变结构控制。我们采用较为简单的双闭环PID控制实现平衡模…

指标异常检测和诊断

检测 是发现问题 诊断 是找到原因 误差的分类 系统误差:系统误差是由于仪器本身不精确,或实验方法粗略,或实验原理不完善而产生的。随机误差:随机误差是由各种偶然因素对实验者、测量仪器、被测物理量的影响而产生的。粗大误差&…

【C++】用wxWidgets实现多文档窗体程序

一、基本步骤和示例代码 在wxWidgets中,要实现多文档窗体程序,通常会使用wxMDIParentFrame和wxMDIChildFrame类来创建一种标准的MDI(多文档接口)应用。以下是基本步骤和示例代码,演示如何使用wxWidgets创建多文档界面…

ChatGLM vs ChatGPT

所有的NLP大模型 都是transformer结构 1.Mask attention 的策略不同 2.训练任务目标不同 国内大模型nb公司:百度、清华智谱 一、主流大模型 粉色:Encoder-only。 绿色:Encoder-Decoder,尽头智谱ChatGLM。 蓝色:…

机械设计-哈工大课程学习-螺旋传动

二、摩擦类型 1、静态摩擦:这是身体静止时所经历的摩擦。换句话说,就是身体有运动倾向时的摩擦力。 2、动态摩擦:这是身体在运动时所经历的摩擦。也称为动摩擦。动摩擦有以下两种类型: ①滑动摩擦:一个物体在另一个…

声明式注解对XXL-JOB的定时任务代码生效吗?

说明:源于博主的思考,本文验证一下声明式注解,即Transactional注解,对XXL-JOB的定时任务是否生效。 准备 首先,创建一个需要事务的场景。有两张表,一张部门表,一张用户表,用户隶属…

huggingface学习 | 云服务器使用hf_hub_download下载huggingface上的模型文件

系列文章目录 huggingface学习 | 云服务器使用git-lfs下载huggingface上的模型文件 文章目录 系列文章目录一、hf_hub_download介绍二、找到需要下载的huggingface文件三、准备工作及下载过程四、全部代码 一、hf_hub_download介绍 hf_hub_download是huggingface官方支持&…

【Godot4自学手册】第二节主人公设置

继续学习Godot,今天是第二节的内容,本节主要完成游戏玩家的设置,将玩家展现在场景中。 一、新建一个主场景 首先在场景面板中单击2D场景,如图。 这样我们就有了一个2D场景,我们将Node2D重新命名为“Main”&#xff…

核密度曲线(python

目录 1.代码:2.效果:小结: 1.代码: import pandas as pd import matplotlib.pyplot as plt # 读入数据 file r123.xlsx sheet Sheet2 col S213 # 标题名称 title col 供订比曲线 xlabel 供订比 # 横轴显示范围 xleft 0 xr…

数据操作——缺失值处理

缺失值处理 缺失值的处理思路 如果想探究如何处理无效值, 首先要知道无效值从哪来, 从而分析可能产生的无效值有哪些类型, 在分别去看如何处理无效值 什么是缺失值 一个值本身的含义是这个值不存在则称之为缺失值, 也就是说这个值本身代表着缺失, 或者这个值本身无意义, 比如…

各省份经济不平等和家庭支出分布数据:恩格尔系数和泰尔指数(2000-2022年)

中国各省的恩格尔系数和泰尔指数数据为我们提供了关于这些地区居民消费结构和收入差距的了解。 恩格尔系数是衡量居民生活水平和消费结构的一个重要指标,它表示食品支出占家庭总支出的比例。通常,恩格尔系数较低表明居民的生活水平较高。 泰尔指数则是…

ChatGPT时代对大数据应用的展望

前言: 2022年底,科技圈有个爆炸性新闻,ChatGPT的诞生,引发了世界范围内的震惊;人工智能在与人交流上有了划时代的技术突破,可以和人深入的理解交流,让许多公司和领域对这项技术有了更多遐想。对…

ElasticSearch(ES) 搜索入门笔记

文章目录 ElasticSearch(ES) 搜索入门笔记环境准备-本地安装ES和Kibanamapping字段类型mapping 参数Analyzer自定义分析器分析器的测试中文分词 ik_maxNormalizer 其他关于mapping的要点 ES 搜索[match all 查询](https://www.elastic.co/guide/en/elasticsearch/reference/cur…

77. 组合 - 力扣(LeetCode)

题目描述 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案。 输入示例 n 4, k 2输出示例 [[2,4],[3,4],[2,3],[1,2],[1,3],[1,4], ]解题思路 我们使用回溯、深度优先遍历的思想,我们使用一个栈 path…
最新文章