19.面试算法-树的深度优先遍历(一)

1. 深入理解前中后序遍历

深度优先遍历有前中后三种情况,大部分人看过之后就能写出来,很遗憾大部分只是背下来的,稍微变换一下就废了。

我们再从二叉树的角度看递归,每次遇到递归,都按照前面说的四步来写,可以更好地写出正确的递归算法。

第一步:从小到大递推,分情况讨论

第二步:明确结束条件,初步确定入参

第三步:将等价关系变成方法调用,完成代码

第四步:从大到小画图推演

我们接下来就一步步看怎么做:

第一步:从小到大递推,分情况讨论

我们就以这个二叉树为例,这个树在很多题目都会用到:

  3
 / \
9  20
  /  \
 15   7

我们知道前中后序就是看父节点与左右孩子相比的访问顺序,前序就是中左右,中序就是左中右,后序就是左右中。我们先选一个最小的子树:

  20
  / \
15   7

假如20为head,则此时前序访问顺序应该是:

list.add(root);//20被访问
preorder(root.left);//继续访问15  
preorder(root.right); //继续访问7

验证一下,正好是20 15 7,没问题,然后再向上访问,看node(3)的情况:

list.add(root);//3被访问
preorder(root.left);//继续访问,得到9
preorder(root.right); //继续访问以node(20)为父节点的子树

此时先得到3,在得到9,之后递归preorder(root.right);的结果就是我们上一步的,合在一起就是需要的结果 3 9 20 15 7。

第二步:明确结束条件,初步确定入参

这里的递归什么时候结束呢?很明显应该是root=null的时候。一般来说链表和二叉树问题的终止条件都包含当前访问的元素为null。

那入参是什么呢?首先就是要访问的这个子树的根结点root,然后要将结果保存起来的,所以还应该有个list,每次满足要求就将结果放进来。

第三步:将等价关系变成方法调用,完成代码

到此为止,我们就能将完整代码写出来了:

public void preorder(TreeNode root, List<Integer> res) {
	if (root == null) {
		return;
	}
	res.add(root.val);
	preorder(root.left, res);
	preorder(root.right, res);
}

第四步 从大到小 画图推演

写完之后对不对呢?我们可以画个调用图看一下,因为是两个递归函数,如果比较复杂。我们可以少画几组。下图中的序号就是递归的完整过程:
在这里插入图片描述
从图中可以看到,当root的一个子树为null的时候还是会执行递归,进入之后发现root==null了,然后就开始返回。这里我们要特别注意res.add()的时机对不对,将其进入顺序依次写出来就是需要的结果。

这个明确之后再debug或者检查就容易很多,刚开始学习递归建议多画几次,熟悉之后就不必再画了。

另外注意一个问题,有时候题目给的方法不方便直接递归,我们需要再写一个自己的方法包一下递归过程。例如上 面前序遍历的问题,题干给的是这样的结构:

class Solution {
	public List<Integer> preorderTraversal(TreeNode root) {
		List<Integer> res = new ArrayList<Integer>();
		preorder(root, res);
		return res;
	}
}

这个方面不方便直接递归,我们希望在每次递归过程里处理res,此时可以自己创建一个preorder()方法,这样做是完全可以的。

前序遍历写出来之后,中序和后序遍历就不难理解了,中序是左中右,后序是左右中。代码如下:

public  void preOrderRecur(TreeNode head) {
	if (head == null) {
		return;
	}
	preOrderRecur(head.left);
	System.out.print(head.val + " ");
	preOrderRecur(head.right);
}

后序:

public static void postOrderRecur(TreeNode head) {
	if (head == null) {
		return;
	}
	postOrderRecur(head.left);
	postOrderRecur(head.right);
	System.out.print(head.value + " ");
}

接下来我们就一起来分析一些经典的二叉树题目。

2. 深度和高度专题

给定二叉树 [3,9,20,null,null,15,7],如下图

  3
 / \
9  20
   / \
  15  7

然后LeetCode给我们造了一堆的题目:

【1】 LeetCode104 二叉树的最大深度:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点 的最长路径上的节点数。说明: 叶子节点是指没有子节点的节点。例如上面的例子返回结果为最大深度为3。

【2】 LeetCode110 判断平衡二叉树:给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡 二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1 。

【3】 LeetCode111 二叉树的最小深度:给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。说明:叶子节点是指没有子节点的节点。上面的例子返回结果为2。

这三个题看起来挺像的,我们就一起来用上面的思路来逐步分析。

2.1 最大深度问题

首先看一下104题找最大深度:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。说明:叶子节点是指没有子节点的节点。例如上面的例子返回结果为最大深度为3。

我们先看一个简单情况:

  3             3           3        
 / \           /             \
9  20         9              20 

对于node(3),最大深度自然是左右子结点+1,左右子节点有的可能为空,只要有一个,树的最大高度就是 1+1=2。

然后再增加几个结点,并增加几种情况:

  3             3           3           3
 / \           / \         / \           \
9  20         9  20       9  20          20 
  /  \          /              \        /  \ 
 15   7        15               7      15   7

很显然相对于node(20),最大深度自然是左右子结点+1,左右子节点有的可能为空,只要有一个,树的最大高度就 是1+1=2,用代码表示就是

int depth = 1 + max(leftDepth, rightDepth);

。而对于3,则是左右子树深度最大的那个然后再+1,具体谁更大,则不必关心。所以对于node(3)的判断逻辑就是:

int leftDepth = getDepth(node.left); // 左
int rightDepth = getDepth(node.right); // 右
int depth = 1 + max(leftDepth, rightDepth); // 中

对于int leftDepth = getDepth(node.left) ,很显然这里的left是node(9),执行一次getDepth()就返回1了。

而对于rightDepth = getDepth(node.right);这里的right是node(20),node(20)的最大值是多少呢?显然要先计算getDepth(node(20))才知道。具体怎么算呢?交给下层来处理吧,getDepth(node(20))重新按照一样的过程来找。

通过二叉树可以非常方便的理解递归的执行过程,从这里我们可以看到,递归只处理当前这一层和下一层之间的关系,并不关心下层和下下层之间的关系。这就像老子只管养好儿子,至于孙子怎么样,那是儿子的事,你也不能瞎掺合。只要将儿子一代养好就上对得起祖宗,下对得起万代了。

然后就是什么时候结束呢,这里也比较简单,仍然是执行到root == null返回0就行了。

至于入参,自然是要处理的子树的根节点root,而返回值则是当前root所在子树的最大高度。所以合并在一起就是:

public int maxDepth(TreeNode root) {
	if (root == null) {
		return 0;
	}
	int leftHeight = maxDepth(root.left);
	int rightHeight = maxDepth(root.right);
	return Math.max(leftHeight, rightHeight) + 1;
}

这个对不对呢?可以画个图推演一下看看。

这个题我们需要先将左右子树的结果拿到之后才能计算Math.max(leftHeight, rightHeight) + 1。这是不是与后序遍历非常像呢?先解决好左右子孩子的问题再处理当前结点,这就是后序遍历的拓展问题。

有人会发现,如果通过层次遍历是不是也能解决呢?我们在层次遍历里说过,分层的时候每次都能获得一层的元素,那统计一下一共有几层自然不是问题。

2.2 平衡树问题

LeetCode110 判断平衡二叉树:给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1。

这里的要求是二叉树每个节点的左右两个子树的高度差的绝对值不超过 1。先补充一个问题,高度和深度怎么区分呢?

  • 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。
  • 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。

有点绕是吗?废话不多说,直接看图,能懂就行:
在这里插入图片描述
关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1,其他地方还有将其视为0的,但是毕竟刷题用LeetCode,就不管其他的了。

言归正传,我们仍然先看一个最简单的情况:

  3             3           3        
 / \           /             \
9  20         9              20 

很显然只有两层的时候一定是平衡的,为什么呢?对于node(3),如果左右孩子只有一个,那么高度差就是1,如果左右子孩子都有或者都没有,则高度差为0。如果增加一层会怎么样呢?

  3             3           3           3
 / \           / \         / \           \
9  20         9  20       9  20          20 
  /  \          /              \        /  \ 
 15   7        15               7      15   7

对于node(3),需要同时知道自己左右子树的最大高度差是否<2。

  • 当节点root 左 / 右子树的高度差 < 2,则返回以节点root为根节点的子树的最大高度,即节点 root 的左右子树中最大高度加 1( max(left, right) + 1 );
  • 当节点root 左 / 右子树的高度差 ≥ 2,则返回 -1 ,代表此子树不是平衡树 。

也就是这里要完成两个工作:判断两个子树高度差与2的关系,返回最大高度或者是否为平衡树,如果是后者则直接退出就行了,如果是前者则还要返回最大高度,让上层能够继续判断。例如上面图中,node(20)是平衡的,所以返回最大高度2。然后上层node(3)发现左子树高度是0,而右子树是2,所以不平衡。所以就有:

int left = recur(root.left);
int right = recur(root.right);
return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;

我们此时就写出了核心递归,然后再看终止条件。很显然,if (root == null) return 0;仍然是必要的,图中 node(9),node(15),node(7)的触底反弹都需要。

假如子树已经不平衡了,我们就不需要再递归了,直接返回就行,比如这个树:

               5            
              / \         
             3   4        
            / \   \    
           1   2   6    
                    \
                     7   

对于node(4)发现高度差大于1了,返回-1,之后node(5)检测到 right=-1 就不必再比较了,因为已经有子树不平衡了,直接返回-1即可。所以我们要加一下left或者right直接就返回了-1的情况,也就是:

private int recur(TreeNode root) {
	if (root == null) return 0;
	int left = recur(root.left);
	if(left == -1) return -1;
	int right = recur(root.right);
	if(right == -1) return -1;
	return Math.abs(left - right) < 2 ? Math.max(left, right) + 1 : -1;
}

之后根据题目要求再套一个方法:

public boolean isBalanced(TreeNode root) {
	return recur(root) != -1;
}

这就是我们想要的结果,这种也是先获得左右子树的情况,最后处理自己,也是后序遍历的拓展。

2.3 最小深度

LeetCode111 二叉树的最小深度:给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最 短路径上的节点数量。 说明:叶子节点是指没有子节点的节点。

上面两个问题都用到了最大深度的问题,那最大深度里直接将Max改成Min行不行呢?自然是不行的,为什么呢? 遍历顺序上依然是后序遍历(因为要比较递归返回之后的结果),但在处理中间节点的逻辑上,最大深度很容易解, 最小深度可有一个误区。

这里的关键问题是题目中说的:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

注意是叶子节点。什么是叶子节点,左右孩子都为空的节点才是叶子节点!所以,如果我们这么判断就错了:

int leftDepth = getDepth(node.left);
int rightDepth = getDepth(node.right);
int result = 1 + min(leftDepth, rightDepth);
return result;

如果这么求的话,没有左孩子的分支会算为最短深度。 所以这里的核心问题仍然是分析终止条件:

  • 如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。
  • 反之,右子树为空,左子树不为空,最小深度是 1 + 左子树的深度。
  • 最后如果左右子树都不为空,返回左右子树深度最小值 + 1 。

代码整理出来就是这样:

class Solution {
	public int minDepth(TreeNode root) {
		if (root == null) {
			return 0;
		}

		if (root.left == null && root.right == null) {
			return 1;
		}

		int min_depth = Integer.MAX_VALUE;
		if (root.left != null) {
			min_depth = Math.min(minDepth(root.left), min_depth);
		}
		if (root.right != null) {
			min_depth = Math.min(minDepth(root.right), min_depth);
		}
		return min_depth + 1;
	}
}

遍历的顺序为后序(左右中),可以看出:求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。

貌似这个题用层次遍历也行是吧?后面再讨论。

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

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

相关文章

从壹开始解读Yolov11【源码研读系列】——cfg:模型配置加载功能

目录 一、模型配置操作&#xff1a;cfg.__init__.py 1.cfg.cfg2dict&#xff1a;yaml转字典 2.cfg.get_cfg&#xff1a;读取覆盖配置 3.cfg全局配置参数查询表 ①*基础参数配置&#xff1a; ②*训练参数配置&#xff1a; ③验证测试参数配置&#xff1a; ④*预测参数配置&…

element plus中menu菜单技巧

我在使用element plus的menu&#xff08;侧边栏&#xff09;组件的过程中遇到了一些问题&#xff0c;就是menu编写样式和路由跳转&#xff0c;下面给大家分享以下&#xff0c;我是怎么解决的。 1.页面效果 我要实现的网站布局是这样的&#xff1a; 侧边栏折叠以后的效果&#…

使用 Spring 框架构建 MVC 应用程序:初学者教程

Spring Framework 是一个功能强大、功能丰富且设计精良的 Java 平台框架。它提供了一系列编程和配置模型&#xff0c;旨在简化和精简 Java 中健壮且可测试的应用程序的开发过程。 人们常说 Java 太复杂了&#xff0c;构建简单的应用程序需要很长时间。尽管如此&#xff0c;Jav…

PHP露营地管理小程序系统源码

&#x1f3d5;️露营新风尚&#xff01;露营地管理小程序系统&#xff0c;打造完美露营体验✨ &#x1f4cd;营地预订&#xff0c;轻松搞定&#x1f4c5; 想要逃离城市的喧嚣&#xff0c;享受大自然的宁静&#xff1f;露营地管理小程序系统让你的露营计划轻松实现&#xff01…

Vulnhub打靶-Empire-LupinOne

基本信息 靶机下载&#xff1a;https://download.vulnhub.com/empire/01-Empire-Lupin-One.zip 攻击机器&#xff1a;192.168.20.128&#xff08;Windows操作系统&#xff09;& 192.168.20.138&#xff08;kali&#xff09; 提示信息&#xff1a; 这个盒子被创建为中等…

FineReport 填报简介vs控件vs页面设置

填报简介 填报功能可以将页面数据写入到数据库&#xff0c;包括数据的增加、删除和修改操作。同时也支持对填写数据的自定义校验&#xff0c;Excel 导入数据&#xff0c;根据填写值智能联动等功能。 填报控件 设计填报报表时&#xff0c;如果需要修改和新增数据&#xff0c;则…

vue3使用element-plus手动更改url后is-active和菜单的focus颜色不同步问题

在实习&#xff0c;给了个需求做个新的ui界面&#xff0c;遇到了一个非常烦人的问题 如下&#xff0c;手动修改url时&#xff0c;is-active和focus颜色不同步 虽然可以直接让el-menu-item:focus为白色能解决这个问题&#xff0c;但是我就是想要有颜色哈哈哈&#xff0c;有些执…

【JAVA面试题】什么是Springboot的自动配置以及注意事项

文章目录 强烈推荐核心概念&#xff1a;自动配置的关键特点&#xff1a;示例&#xff1a; 需要注意的点1.默认配置可能不适合所有场景2.Bean 冲突与覆盖3.应用启动慢的问题4.过度依赖自动配置5.安全性问题6.依赖冲突与版本兼容7.过多不必要的自动配置8.调试困难 专栏集锦 强烈推…

python实战项目43:采集汽车之家数据

python采集汽车之家数据 一、寻找数据接口二、发送请求获取响应三、解析数据四、完整代码一、寻找数据接口 如下图所示,在汽车之家首页点击报价图标: 在下图中选择价位,例如选择15-20万: 打开浏览器开发者工具,刷新页面,找到数据接口。接下来,通过翻页寻找接口url的变…

如果你不幸成为家里第一个GIS专业的学生

家里无法给我很多建设性意见&#xff0c;大学四年到工作都是自己一个人跌跌撞撞走过来的&#xff0c;期间因为信息差走了不少弯路。对于GIS专业而言&#xff0c;没有家里人的指路&#xff0c;信息差就成了同学之间拉开差距的重要因素。现在我们要做的就是打破专业信息差&#x…

Vue+ECharts+iView实现大数据可视化大屏模板

Vue数据可视化 三个大屏模板 样式还是比较全的 包括世界地图、中国地图、canvas转盘等 项目演示&#xff1a; 视频&#xff1a; vue大数据可视化大屏模板

uiautomatorviewer安卓9以上正常使用及问题处理

一、安卓9以上使用uiautomatorviewer问题现象 打开Unexpected error while obtaining UI hierarchy 问题详情 Unexpected error while obtaining UI hierarchy java.lang.reflect.InvocationTargetException 二、问题处理 需要的是替换对应D:\software\android-sdk-windows…

这种V带的无极变速能用在新能源汽车上吧?

CVT的无极变速器的结构能用在电动汽车上吗&#xff1f;

Python 将网页保存为图片(Chrome内核)

一、背景介绍 之前写过一篇将网页保存为图片的文章 C# 将网页保存为图片&#xff08;利用WebBrowser&#xff09;_c# webbrowser 把网页内容转换成图片-CSDN博客​​​​​​ 这里有个弊端&#xff0c;C# WebBrowser使用的是IE内核&#xff0c;目前很多网站都不支持IE了&…

深度学习(二)框架与工具:开启智能未来之门(2/10)

一、深度学习框架&#xff1a;引领智能变革的利器 深度学习框架在人工智能领域中扮演着至关重要的角色&#xff0c;堪称引领智能变革的利器。随着人工智能技术的飞速发展&#xff0c;深度学习框架不断崛起并迅速壮大。 主流的深度学习框架如 TensorFlow、PyTorch、Keras 等&a…

社招高频面试题

1.单例模式 面试突击50&#xff1a;单例模式有几种写法&#xff1f; 2.Mybatis缓存机制 MyBatis的一、二级缓存查询关系 一级缓存是SqlSession级别&#xff0c;不能跨SqlSession共享&#xff0c;默认开启。 二级缓存是基于mapper namespace级别的&#xff0c;可以跨SqlSessi…

第J6周:ResNeXt-50实战解析(pytorch版)

>- **&#x1f368; 本文为[&#x1f517;365天深度学习训练营]中的学习记录博客** >- **&#x1f356; 原作者&#xff1a;[K同学啊]** 任务&#xff1a; ●阅读ResNeXt论文&#xff0c;了解作者的构建思路 ●对比我们之前介绍的ResNet50V2、DenseNet算法 ●使用ResNeX…

基于Java+SpringBoot+Vue的古典舞在线交流平台的设计与实现

基于JavaSpringBootVue的古典舞在线交流平台的设计与实现 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末附源码下载链接&a…

科研类型PPT的制作技巧

目录 科研类型PPT的制作技巧 荣誉: 首页:ppt开头结尾 小标题 重点标记:加粗红色下划线 使用三线表 图片,文本排版 一、明确目的与受众分析 二、基础设计原则 三、内容组织与呈现 四、绘图与模型制作 五、其他注意事项 科研类型PPT的制作技巧 荣誉: 首页:ppt开…

spark读取parquet文件

源码 parquet文件读取的入口是FileSourceScanExec&#xff0c;用parquet文件生成对应的RDD 非bucket文件所以走createNonBucketedReadRDD方法。 createNonBucketedReadRDD 过程&#xff1a; 确定文件分割参数 openCostInBytes4M 相关参数spark.sql.files.openCostInBytes4M…