热门面试题第14天|Leetcode 513找树左下角的值 112 113 路径总和 105 106 从中序与后序遍历序列构造二叉树 (及其扩展形式)以一敌二
找树左下角的值
本题递归偏难,反而迭代简单属于模板题, 两种方法掌握一下
题目链接/文章讲解/视频讲解:https://programmercarl.com/0513.%E6%89%BE%E6%A0%91%E5%B7%A6%E4%B8%8B%E8%A7%92%E7%9A%84%E5%80%BC.html

我们来分析一下题目:在树的最后一行找到最左边的值。
首先要是最后一行,然后是最左边的值。
如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。
如果对二叉树深度和高度还有点疑惑的话,请看:110.平衡二叉树 (opens new window)。
所以要找深度最大的叶子节点。
那么如何找最左边的呢?可以使用前序遍历(当然中序,后序都可以,因为本题没有 中间节点的处理逻辑,只要左优先就行),保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。
1.确定递归函数的参数和返回值
参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。
本题还需要类里的两个全局变量,maxDepth用来记录最大深度,result记录最大深度最左节点的数值。
private int maxDepth = Integer.MIN_VALUE;private int result;public void traversal(TreeNode root, int depth)
2.确定终止条件
当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。
代码如下:
if (root.left == null && root.right == null) {if (depth > maxDepth) {maxDepth = depth;result = root.val;}return;}
3.确定单层递归的逻辑
在找最大深度的时候,递归的过程中依然要使用回溯,代码如下:
if (root.left != null) {depth++;traversal(root.left, depth);depth--; // 回溯}if (root.right != null) {depth++;traversal(root.right, depth);depth--; // 回溯}}
我们来看完整代码
class Solution {private int maxDepth = Integer.MIN_VALUE;private int result;public void traversal(TreeNode root, int depth) {if (root.left == null && root.right == null) {if (depth > maxDepth) {maxDepth = depth;result = root.val;}return;}if (root.left != null) {depth++;traversal(root.left, depth);depth--; // 回溯}if (root.right != null) {depth++;traversal(root.right, depth);depth--; // 回溯}}public int findBottomLeftValue(TreeNode root) {traversal(root, 0);return result;}
}
路径总和
本题 又一次涉及到回溯的过程,而且回溯的过程隐藏的还挺深,建议先看视频来理解
112. 路径总和,和 113. 路径总和ii 一起做了。 优先掌握递归法。
题目链接/文章讲解/视频讲解:https://programmercarl.com/0112.%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8C.html

1.确定递归函数的参数和返回类型
本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?
如图所示:

图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。
所以代码如下:
bool traversal(treenode* cur, int count) // 注意函数的返回类型
2.确定终止条件
首先计数器如何统计这一条路径的和呢?
不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。
如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。
如果遍历到了叶子节点,count不为0,就是没找到。
递归终止条件代码如下:
if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
3.确定单层递归的逻辑
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。
递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
if (cur->left) { // 左count -= cur->left->val; // 递归,处理节点;if (traversal(cur->left, count)) return true;count += cur->left->val; // 回溯,撤销处理结果
}
if (cur->right) { // 右count -= cur->right->val;if (traversal(cur->right, count)) return true;count += cur->right->val;
}
return false;
我们来看完整代码
class Solution {private boolean traversal(TreeNode cur, int count) {if (cur.left == null && cur.right == null && count == 0) {return true; // 遇到叶子节点,并且计数为0}if (cur.left == null && cur.right == null) {return false; // 遇到叶子节点直接返回}if (cur.left != null) { // 左count -= cur.left.val; // 递归,处理节点if (traversal(cur.left, count)) return true;count += cur.left.val; // 回溯,撤销处理结果}if (cur.right != null) { // 右count -= cur.right.val; // 递归,处理节点if (traversal(cur.right, count)) return true;count += cur.right.val; // 回溯,撤销处理结果}return false;}public boolean hasPathSum(TreeNode root, int sum) {if (root == null) return false;return traversal(root, sum - root.val);}
}
注意,我们在调用的时候用的是 traversal(root, sum - root.val), sum - root.val代表的是还需要寻找的count值
我们来看一道类似的

路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!

我们只需要用集合去存储数组,最后输出就行
我们来看代码
class Solution {private List<List<Integer>> result = new ArrayList<>();private List<Integer> path = new ArrayList<>();// 递归函数不需要返回值,因为我们要遍历整个树private void traversal(TreeNode cur, int count) {if (cur.left == null && cur.right == null && count == 0) { // 遇到了叶子节点且找到了和为sum的路径result.add(new ArrayList<>(path));return;}if (cur.left == null && cur.right == null) return; // 遇到叶子节点而没有找到合适的边,直接返回if (cur.left != null) { // 左 (空节点不遍历)path.add(cur.left.val);count -= cur.left.val;traversal(cur.left, count); // 递归count += cur.left.val; // 回溯path.remove(path.size() - 1); // 回溯}if (cur.right != null) { // 右 (空节点不遍历)path.add(cur.right.val);count -= cur.right.val;traversal(cur.right, count); // 递归count += cur.right.val; // 回溯path.remove(path.size() - 1); // 回溯}return;}public List<List<Integer>> pathSum(TreeNode root, int sum) {result.clear();path.clear();if (root == null) return result;path.add(root.val); // 把根节点放进路径traversal(root, sum - root.val);return result;}
}
从中序与后序遍历序列构造二叉树
本题算是比较难的二叉树题目了,大家先看视频来理解。
106.从中序与后序遍历序列构造二叉树,105.从前序与中序遍历序列构造二叉树 一起做,思路一样的
题目链接/文章讲解/视频讲解:https://programmercarl.com/0106.%E4%BB%8E%E4%B8%AD%E5%BA%8F%E4%B8%8E%E5%90%8E%E5%BA%8F%E9%81%8D%E5%8E%86%E5%BA%8F%E5%88%97%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91.html

首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。
如果让我们肉眼看两个序列,画一棵二叉树的话,应该分分钟都可以画出来。
流程如图:

那么代码应该怎么写呢?
说到一层一层切割,就应该想到了递归。
来看一下一共分几步:
-
第一步:如果数组大小为零的话,说明是空节点了。
-
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
-
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
-
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
-
第五步:切割后序数组,切成后序左数组和后序右数组
-
第六步:递归处理左区间和右区间
不难写出如下代码:(先把框架写出来)
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {// 第一步if (postorder.size() == 0) return NULL;// 第二步:后序遍历数组最后一个元素,就是当前的中间节点int rootValue = postorder[postorder.size() - 1];TreeNode* root = new TreeNode(rootValue);// 叶子节点if (postorder.size() == 1) return root;// 第三步:找切割点int delimiterIndex;for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) {if (inorder[delimiterIndex] == rootValue) break;}// 第四步:切割中序数组,得到 中序左数组和中序右数组// 第五步:切割后序数组,得到 后序左数组和后序右数组// 第六步root->left = traversal(中序左数组, 后序左数组);root->right = traversal(中序右数组, 后序右数组);return root;
}
我们进行切割的时候要先切割中序再切割后序,因为只有确定了中序的左右有几个元素,我们才可以在后序遍历里面定位
注意切割的时候需要记住中序左右数组定位需要+1,要跳过中的位置
我们来看完整代码
class Solution {private TreeNode traversal(int[] inorder, int inorderBegin, int inorderEnd, int[] preorder, int preorderBegin, int preorderEnd) {// 如果是空区间,返回nullif (preorderBegin == preorderEnd) return null;// 从前序遍历的第一个元素获取根节点的值int rootValue = preorder[preorderBegin];TreeNode root = new TreeNode(rootValue);// 如果是叶子节点,直接返回if (preorderEnd - preorderBegin == 1) return root;// 在中序遍历中找到根节点的位置int delimiterIndex;for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {if (inorder[delimiterIndex] == rootValue) break;}// 切割中序数组// 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd)int leftInorderBegin = inorderBegin;int leftInorderEnd = delimiterIndex;// 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd)int rightInorderBegin = delimiterIndex + 1;int rightInorderEnd = inorderEnd;// 切割前序数组// 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd)int leftPreorderBegin = preorderBegin + 1;int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin;// 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd)int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);int rightPreorderEnd = preorderEnd;// 递归构建左右子树root.left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd);root.right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd);return root;}public TreeNode buildTree(int[] preorder, int[] inorder) {if (inorder.length == 0 || preorder.length == 0) return null;// 参数遵循左闭右开的原则return traversal(inorder, 0, inorder.length, preorder, 0, preorder.length);}
}
我们来看一个相关的题目

这道题我们是从前序,中序来遍历
主要修改点:
-
函数参数顺序:
- 修改了
buildTree方法的参数顺序,从(inorder, postorder)改为(preorder, inorder) - 相应地修改了
traversal方法的参数顺序
- 修改了
-
根节点位置:
- 后序遍历中根节点在最后:
postorder[postorder.length - 1] - 前序遍历中根节点在最前:
preorder[0]
- 后序遍历中根节点在最后:
-
数组切割方式:
- 后序数组切割:
leftPostorder从 0 开始,rightPostorder到倒数第二个元素结束 - 前序数组切割:
leftPreorder从第二个元素(索引1)开始,rightPreorder到最后一个元素
- 后序数组切割:
-
递归调用:
- 修改了递归调用的参数顺序,确保传入正确的前序和中序数组
我们只要在这个基础上进行修改,就可以实现效果,我们来看代码
class Solution {private TreeNode traversal(int[] inorder, int[] preorder) {// 如果前序数组为空,返回空节点if (preorder.length == 0) return null;// 前序遍历数组第一个元素,就是当前的根节点int rootValue = preorder[0];TreeNode root = new TreeNode(rootValue);// 叶子节点if (preorder.length == 1) return root;// 找到中序遍历的切割点int delimiterIndex;for (delimiterIndex = 0; delimiterIndex < inorder.length; delimiterIndex++) {if (inorder[delimiterIndex] == rootValue) break;}// 切割中序数组// 左闭右开区间:[0, delimiterIndex)int[] leftInorder = Arrays.copyOfRange(inorder, 0, delimiterIndex);// [delimiterIndex + 1, end)int[] rightInorder = Arrays.copyOfRange(inorder, delimiterIndex + 1, inorder.length);// 切割前序数组// 依然左闭右开,注意这里使用了左中序数组大小作为切割点// [1, 1+leftInorder.length) 注意这里从1开始,因为0是根节点int[] leftPreorder = Arrays.copyOfRange(preorder, 1, 1 + leftInorder.length);// [1+leftInorder.length, end)int[] rightPreorder = Arrays.copyOfRange(preorder, 1 + leftInorder.length, preorder.length);root.left = traversal(leftInorder, leftPreorder);root.right = traversal(rightInorder, rightPreorder);return root;}public TreeNode buildTree(int[] preorder, int[] inorder) {if (inorder.length == 0 || preorder.length == 0) return null;return traversal(inorder, preorder);}
}
相关文章:
热门面试题第14天|Leetcode 513找树左下角的值 112 113 路径总和 105 106 从中序与后序遍历序列构造二叉树 (及其扩展形式)以一敌二
找树左下角的值 本题递归偏难,反而迭代简单属于模板题, 两种方法掌握一下 题目链接/文章讲解/视频讲解:https://programmercarl.com/0513.%E6%89%BE%E6%A0%91%E5%B7%A6%E4%B8%8B%E8%A7%92%E7%9A%84%E5%80%BC.html 我们来分析一下题目&#…...
shopify跨境电商行业前景与规模
Shopify跨境电商行业前景与规模分析 一、行业背景 Shopify 是一个全球知名的电子商务平台,它为小型企业到大型企业提供了创建和管理在线商店的工具。近年来,随着全球化进程的加快以及互联网技术的发展,跨境电商已经成为国际贸易的重要组成部…...
【计算机网络】-计算机网络期末复习题复习资料
一、计算机网络体系结构(800字) 1. OSI参考模型 七层结构:物理层→数据链路层→网络层→传输层→会话层→表示层→应用层 各层核心功能: 物理层:比特流传输(如RJ45、光纤接口) 数据链路层&…...
游戏中的碰撞检测算法
参考博客Sort, sweep, and prune: Collision detection algorithms...
批归一化(Batch Normalization)与层归一化(Layer Normalization)的区别与联系
文章目录 一、Batch normalization 理论与应用1. 理论解释2. 数值例子 二、Layer normalization 理论与应用1. 理论解释2. 数值例子 三、Layer Normalization 和 Batch Normalization 的区别四、《Transformers without Normalization》笔记 一、Batch normalization 理论与应用…...
12届蓝桥杯—货物摆放
货物摆放 题目描述 小蓝有一个超大的仓库,可以摆放很多货物。 现在,小蓝有 nn 箱货物要摆放在仓库,每箱货物都是规则的正方体。小蓝规定了长、宽、高三个互相垂直的方向,每箱货物的边都必须严格平行于长、宽、高。 小蓝希望所…...
c++进阶--哈希表的实现
大家好,今天我们来学习ubordered_set和unordered_map的底层哈希表。 目录 哈希表实现 1. 哈希概念 1.1 直接定址法 1.2 哈希冲突 1.3 负载因⼦ 1.4 将关键字转为整数 1.5 哈希函数 下面我们介绍几种哈希函数:1.5.1 除法散列法/除留余数法 1.…...
颠覆传统:SaaS 品牌如何通过 SEO 策略引爆市场!
SaaS 商业模式提供了令人难以置信的可扩展性和盈利能力——但前提是与正确的营销增长策略相结合。 SaaS 品牌知道,托管基于云的应用程序的成本会随着用户量的增加而降低,因此必须专注于订阅者的快速增长,以保持竞争力并降低成本。 许多 CMO…...
【数据库发展史】
数据库的发展历史可以追溯到20世纪50年代,随着计算机技术的进步和数据管理需求的演变,数据库系统经历了多个阶段的变革。以下是数据库技术的主要发展阶段: 1. 前数据库时代(1950年代前) 手工管理:数据通过…...
HTTP 核心知识点整理
1. HTTP 基础 定义:HTTP(HyperText Transfer Protocol)是应用层协议,基于 请求-响应模型,用于客户端(浏览器)与服务器之间的通信。特点: 无状态:每次请求独立&a…...
从AEC-Q100看车规芯片的可靠性设计要点
引言 随着汽车电子化、智能化的飞速发展,汽车电子控制系统对芯片的可靠性提出了极为严苛的要求。AEC-Q100是汽车电子委员会(Automotive Electronics Council)制定的车规级芯片可靠性标准,旨在确保芯片能够在复杂多变的汽车环境中…...
陕西安全员A证考试的报名流程是什么?
陕西安全员 A 证考试报名流程如下: 进入报名系统:登录陕西省建筑工程施工企业安全管理人员及特种作业人员考试报名系统。首次使用需点击 “特种作业人员注册”,进入个人注册界面。注册账号:输入身份证号、登录密码,并…...
特殊行车记录仪DAT视频丢失的恢复方法
行车记录仪是一种常见的车载记录仪,和常见的“小巧玲珑”的行车记录仪不同,一些特种车辆使用的记录仪的外观可以用“笨重”来形容。下边我们来看看特种车载行车记录仪删除文件后的恢复方法。 故障存储: 120GB存储设备/文件系统:exFAT /簇大小:128KB 故…...
PAT乙级1007
常规解法 #include <iostream> using namespace std;// 判断一个数是否为素数的函数 bool isprime(int a) {// 遍历 2 到 sqrt(a) 之间的数,判断 a 是否能被它们整除for (int i 2; i * i < a; i) {if (a % i 0) // 如果能整除,说明 a 不是素…...
数据库中不存在该字段
mybatisplus 定义的类中某些字段是数据库里面没有的,我们可用tablefield(existfalse)来注解,演示如下:...
吾爱出品,文件分类助手,高效管理您的 PC 资源库
在日常使用电脑的过程中,文件杂乱无章常常让人感到困扰。无论是桌面堆积如山的快捷方式,还是硬盘中混乱的音频、视频、文档等资源,都急需一种高效的整理方法。文件分类助手应运而生,它是一款文件管理工具,能够快速、智…...
关于瑞芯微开发工具(RKDevTool)刷机下载Boot失败原因的研究
昨天发了文章《网心云OEC/OEC-turbo刷机问题——刷机教程、救砖方法、技术要点及下载boot失败异常解决尝试》,其中有关于刷机各种问题的一些解决方法。 网心云OEC/OEC-turbo刷机问题——刷机教程、救砖方法、技术要点及下载boot失败异常解决尝试-CSDN博客文章浏览阅…...
web爬虫笔记:js逆向案例十一 某数cookie(补环境流程)
web爬虫笔记:js逆向案例十一 某数cookie(补环境流程) 一、获取网页数据请求流程 二、目标网址、cookie生成(逐步分析) 1、目标网址:aHR0cHM6Ly9zdWdoLnN6dS5lZHUuY24vSHRtbC9OZXdzL0NvbHVtbnMvNy9JbmRleC5odG1s 2、快速定位入口方法 1、通过脚本监听、hook_cookie等操作可…...
浅谈 Vue3 中的设计模式
设计模式是软件开发中的一种最佳实践,它提供了解决特定问题的通用解决方案。通过合理运用设计模式,可以提高代码的可维护性、可扩展性和可读性。在 Vue3 的源码中,设计模式被广泛应用于各个模块中,充分体现了其在现代前端框架中的…...
Unix Domain Socket、IPC、RPC与gRPC的深度解析与实战
Unix Domain Socket、IPC、RPC与gRPC的深度解析与实战 引言 在分布式系统和本地服务通信中,进程间通信(IPC)与远程过程调用(RPC)是核心能力。本文将深入剖析 Unix Domain Socket(UDS)、IPC、RP…...
07_JavaScript函数作用域_递归
目录 一、作用域(重点) 二、变量的使用规则 (重点) 2.1 访问规则 2.2 赋值规则 三、递归函数 (难点) 了解 四、对象 4.1 对象的创建 一、作用域(重点) 什么是作用域 ? 作用…...
.gitignore使用指南
.gitignore使用指南 目录 什么是.gitignore为什么需要.gitignore如何创建.gitignore文件.gitignore文件的语法规则 忽略单个文件忽略目录忽略特定类型的文件不忽略特定文件或目录递归匹配 示例.gitignore文件注意事项更多特殊场景匹配规则 忽略多个特定后缀的文件忽略特定目录…...
Excel多级联动下拉菜单的自动化设置(使用Python中的openpyxl模块)
1 主要目的 在Excel中,经常会遇到需要制作多级联动下拉菜单的情况,要求单元格内填写的内容只能从指定的多个选项中进行选择,并且需要设置多级目录,其中下级目录的选项内容要根据上级目录的填写内容确定,如下图所示&am…...
深入解析 Spring Framework 5.1.8.RELEASE 的源码目录结构
深入解析 Spring Framework 5.1.8.RELEASE 的源码目录结构 1. 引言 Spring Framework 是 Java 领域最流行的企业级开发框架之一,广泛用于 Web 开发、微服务架构、数据访问等场景。本文将深入解析 Spring Framework 5.1.8.RELEASE 的源码目录结构,帮助开…...
excalidraw画图工具——背景画布有无格子设置
服啦找了大半天,愣是没找到 toggle grid : 切换格子… Excalidraw的背景格子 只要右键,将这个勾取消就好了?...
计算机组成原理———I\O系统精讲<1>
本篇文章主要介绍输入输出系统的发展概况 一.输入输出系统的发展概况 1.早期阶段 该阶段的特点是I/O设备与主存交换信息都必须通过CPU 当时的I/O设备有如下几个特点: (1)每个I\O设备都必须配有一套独立的逻辑电路与CPU相连,用来…...
[数据结构] 动态顺序表应用
可扩容顺序表顺序表 SeqList.hSeqList.cTest.c 动态顺序表能够根据数据存储的需要动态地管理内存空间。 SeqList.h #include<stdio.h> #include<stdlib.h>//静态顺序表 //小了不够用,多了浪费 //#define N 10 //typedef int SLDatatype; //struct SeqL…...
MinIO-对象存储方案
MinIO 是一个基于Apache License v2.0开源协议的对象存储服务。它兼容亚马逊S3云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几kb到最大5T不等。 MinIO是一个非常轻量的服务…...
装饰器模式 (Decorator Pattern)
装饰器模式 (Decorator Pattern) 是一种结构型设计模式,它动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式相比生成子类更为灵活。 一、基础 1 意图 动态地给一个对象添加一些额外的职责。 就增加功能来说,装饰器模式相比生成子类更为灵活。 2 适用场景 当…...
手动配置树莓派wifi联网连接热点手机热点
手动配置树莓派wifi联网连接热点 修改wifi配置文件: 运行命令: sudo nano /etc/wpa_supplicant/wpa_supplicant.conf 在文件中添加无线网配置信息: ctrl_interfaceDIR/var/run/wpa_supplicant GROUPnetdev update_config1 countryCN network{ ssid”你的无线网名字” psk”…...
