目录
1、二叉树遍历:包括前序遍历、中序遍历、后序遍历、层序遍历等
18、从根到叶的二进制数之和(Sum Root to Leaf Numbers)
19、判断二叉搜索树(Binary Search Tree,简称BST)
干货分享,感谢您的阅读!祝你编程题必过!
一、背景知识
树(Tree)是一种非线性的数据结构,由节点和边组成,每个节点可能有多个后继节点。树结构通常用于表示具有层级关系的数据,比如文件系统、网站导航菜单、组织结构等。
树结构的特点是:一个节点可以有多个子节点,但是每个节点只能有一个父节点;除了根节点外,每个节点都有一个父节点;从根节点到任何一个节点都有一条唯一的路径。
树结构有许多种形式,其中最常见的包括二叉树、二叉搜索树、平衡树、红黑树等。
- 二叉树是一种特殊的树结构,每个节点最多有两个子节点,称为左子节点和右子节点。二叉树可以用于实现一些高效的算法,例如二叉搜索树。二叉搜索树是一种特殊的二叉树,其中每个节点的左子树都比该节点的值小,右子树都比该节点的值大,因此可以用于高效地查找、插入和删除元素。
- 平衡树是一种特殊的二叉搜索树,它可以保证在插入、删除元素后树的高度始终保持较小的变化,从而保证了树的搜索、插入、删除等操作的时间复杂度都是 O(log n)。
- 红黑树也是一种平衡树,它通过颜色标记节点来保持平衡,是广泛应用于编程语言和操作系统中的一种数据结构。
除了上述树结构外,还有许多其他类型的树结构,如 B 树、B+ 树、Trie 树等,它们都有自己的特点和应用场景。
把一棵树倒过来的结构,就是称为“树”的数据结构。在很多情况下,性能所对应的主要处理内容就是“查找相应的数据”。可以说,树结构就是为了方便查找而创造出来的。即使数据量增加了,层级也不会随之增加,这就是它的特点。如下图,一个一个的连接点或端点称为节点(Node)。数据是按顺序排列的。(该内容来自图灵社区)
我们来看一下 1000 个数据与 100 万个数据的情况下树的高度。“高度”指的就是层级,表示访问多少次后能找到目标数据。1000 个数据的话是 32 次。100 万个数据的话是 1000 次。虽然数据变为 1000 倍,但是高度只变为 30 倍。
二、树的应用举例
(一)Spring中的应用举例
在Spring框架中,树数据结构被广泛应用于配置元数据的管理。Spring使用一种基于XML或注解的配置机制来管理应用程序的配置信息,这些配置信息包括Bean定义、拦截器、AOP切面等。
在这种配置机制中,配置信息以树形结构的形式进行组织。每个配置文件都包含一个根元素,称为 ApplicationContext 或 BeanFactory。该根元素包含许多子元素,每个子元素都代表一个Bean定义或其他配置信息。
Spring使用了一种名为 BeanDefinition 的数据结构来表示每个Bean定义。BeanDefinition包含Bean的类名、属性、构造函数、作用域等信息。多个BeanDefinition可以以树形结构的形式组织在一起,形成一个完整的应用程序配置。
Spring还使用树形结构来表示应用程序的拦截器和AOP切面。拦截器和切面可以被组织成树形结构,其中每个节点代表一个拦截器或切面,它们可以按照预定义的顺序对目标方法进行拦截和处理。
总之,Spring框架中的树形结构主要用于组织和管理配置信息,使得应用程序的配置更加灵活和可扩展。
(二)其他框架中的应用举例
以下是一些其他框架中树结构的应用举例:
- Hibernate:Hibernate是一种流行的ORM(对象关系映射)框架,它使用树形结构来表示实体类之间的关系。Hibernate中的实体类可以通过多对一、一对多、多对多等关系进行组织,形成一个完整的对象图。这些关系可以使用注解或XML文件进行配置,从而实现数据表之间的关联。
- JFace:JFace是一个为Swing开发的Java GUI框架,它使用树形结构来表示应用程序的数据。JFace中的TreeViewer组件可以用于显示树形数据,它允许用户通过展开和折叠树节点来浏览数据。JFace还提供了许多方便的方法,用于更新树节点和处理用户事件。
- AngularJS:AngularJS是一种流行的JavaScript框架,它使用树形结构来表示应用程序的模板。AngularJS中的模板是一种HTML标记语言,它允许开发人员通过嵌套标记来组织页面内容。这种嵌套结构可以被看作是一个树形结构,其中每个标记都是一个节点。
- Netty:Netty是一个基于NIO的网络编程框架,它使用树形结构来表示网络事件的处理逻辑。Netty中的ChannelPipeline是一个由多个ChannelHandler组成的树形结构,每个ChannelHandler都可以对进入或离开Channel的数据进行处理。这种树形结构可以方便地扩展和修改,以适应不同的网络协议和应用场景。
树形结构在各种框架中都得到了广泛应用,它们用于组织数据、表示关系、处理事件等。
(三)实际开发中的应用举例
在实际开发中,树数据结构被广泛应用于各种场景,例如:
- 目录结构:树结构非常适合表示文件系统的目录结构,其中每个文件夹和文件都可以看作一个节点。可以使用树形结构来遍历文件系统中的所有文件和文件夹,以实现文件管理和搜索等功能。
- 组织架构:树结构也常用于表示企业组织架构,其中每个部门和员工都可以看作一个节点。可以使用树形结构来查询员工信息、管理人员权限等。
- 菜单导航:在Web应用程序中,树结构可以用于构建菜单导航,其中每个菜单项都可以看作一个节点。可以使用树形结构来动态生成菜单,并根据用户权限进行权限控制。
- 算法实现:树结构也是许多算法的重要数据结构之一,例如二叉搜索树、AVL树、红黑树等。可以使用这些树形结构来实现各种算法,例如排序、查找、统计等。
在实际开发中,树数据结构可以被用于各种场景,它们为开发提供了一种简单而强大的方式来组织和处理数据。可以根据自己的需求选择适合的树形结构,以实现高效和可扩展的应用程序。
三、相关编程练习
我们先给出一个TreeNode的结构定义如下
package org.zyf.javabasic.letcode.tree.base;
/**
* @author yanfengzhang
* @description TreeNode 类代表二叉树的节点,
* 其中 val 表示节点的值,
* left 和 right 分别表示左子节点和右子节点。
* @date 2023/4/10 23:21
*/
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int x) {
val = x;
}
}
1、二叉树遍历:包括前序遍历、中序遍历、后序遍历、层序遍历等
题目描述:二叉树遍历是二叉树的基础操作,包括前序遍历、中序遍历、后序遍历、层序遍历等,相关考题如下:
- 前序遍历:二叉树前序遍历,可以使用递归或栈实现。题目:144. Binary Tree Preorder Traversal
- 中序遍历:二叉树中序遍历,可以使用递归或栈实现。题目:94. Binary Tree Inorder Traversal
- 后序遍历:二叉树后序遍历,可以使用递归或栈实现。题目:145. Binary Tree Postorder Traversal
- 层序遍历:二叉树层序遍历,可以使用队列实现。题目:102. Binary Tree Level Order Traversal
解题思路
下面是四种二叉树遍历的最优解法思路:
- 前序遍历:使用栈迭代实现,维护一个栈,先将根节点压入栈中,然后依次弹出栈顶元素,并将其右子树和左子树压入栈中,最后得到前序遍历序列。
- 中序遍历:使用栈迭代实现,维护一个栈和一个指针。将根节点及其左子树依次压入栈中,当栈不为空时,弹出栈顶元素,将其右子树压入栈中,然后继续遍历左子树。
- 后序遍历:使用栈迭代实现,维护一个栈和一个指针,从根节点开始,将根节点及其左子树压入栈中,当栈不为空时,弹出栈顶元素,若其右子树为空或者已经遍历过了,则访问该节点,并将其标记为已访问,否则将右子树和当前节点重新压入栈中,然后继续遍历左子树。
- 层序遍历:使用队列实现,将根节点入队,然后依次弹出队头元素,访问该节点并将其左右子节点入队,依次访问队列中的元素,直到队列为空。
以上方法都是使用迭代的方式实现遍历操作,相比递归,迭代可以避免函数调用的开销和栈溢出的风险,同时也更符合计算机底层的执行方式,因此更加高效。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.Stack;
/**
* @author yanfengzhang
* @description 二叉树遍历:包括前序遍历、中序遍历、后序遍历、层序遍历等
* @date 2023/4/10 23:25
*/
public class BinaryTreeTraversal {
/**
* 前序遍历
*
* @param root 根节点
* @return 遍历结果
*/
public List<Integer> preorderTraversal(TreeNode root) {
/*存储遍历结果的列表*/
List<Integer> res = new ArrayList<>();
/*如果根节点为空,直接返回空列表*/
if (root == null) {
return res;
}
/*使用栈来辅助迭代*/
Stack<TreeNode> stack = new Stack<>();
/*先将根节点入栈*/
stack.push(root);
/*只要栈不为空,就继续遍历*/
while (!stack.isEmpty()) {
/*取出栈顶节点*/
TreeNode node = stack.pop();
/*将节点的值加入遍历结果列表*/
res.add(node.val);
/*如果右子节点不为空,将其入栈*/
if (node.right != null) {
stack.push(node.right);
}
/*如果左子节点不为空,将其入栈*/
if (node.left != null) {
stack.push(node.left);
}
}
/*返回遍历结果*/
return res;
}
/**
* 中序遍历
*
* @param root 根节点
* @return 遍历结果
*/
public List<Integer> inorderTraversal(TreeNode root) {
/*存储遍历结果的列表*/
List<Integer> res = new ArrayList<>();
/*使用栈来辅助迭代*/
Stack<TreeNode> stack = new Stack<>();
/*从根节点开始遍历*/
TreeNode cur = root;
/*只要当前节点不为空或者栈不为空,就继续遍历*/
while (cur != null || !stack.isEmpty()) {
/*先遍历左子树*/
while (cur != null) {
/*将当前节点入栈*/
stack.push(cur);
/*继续遍历左子树*/
cur = cur.left;
}
/*取出栈顶节点,即当前节点*/
cur = stack.pop();
/*将节点的值加入遍历结果列表*/
res.add(cur.val);
/*继续遍历右子树*/
cur = cur.right;
}
/*返回遍历结果*/
return res;
}
/**
* 后序遍历
*
* @param root 根节点
* @return 遍历结果
*/
public List<Integer> postorderTraversal(TreeNode root) {
/*存储遍历结果的列表*/
List<Integer> res = new ArrayList<>();
/*如果根节点为空,直接返回空列表*/
if (root == null) {
return res;
}
/*使用栈来辅助迭代*/
Stack<TreeNode> stack = new Stack<>();
/*先将根节点入栈*/
stack.push(root);
/*使用集合来记录已经访问过的节点*/
Set<TreeNode> visited = new HashSet<>();
/*只要栈不为空,就继续遍历*/
while (!stack.isEmpty()) {
/*取出栈顶节点,但不弹出*/
TreeNode node = stack.peek();
/*如果当前节点是叶子节点 or 或者当前节点已经访问过
/*or 或者当前节点的右子节点已经访问过 or 或者当前节点只有左子节点且左子节点已经访问过*/
if ((node.left == null && node.right == null) ||
visited.contains(node) ||
(node.right != null && visited.contains(node.right)) ||
(node.left != null && node.right == null && visited.contains(node.left))) {
/*将节点的值加入遍历结果列表*/
res.add(node.val);
/*将当前节点弹出栈*/
stack.pop();
/*标记当前节点已经访问过*/
visited.add(node);
}
/*否则,将当前节点的子节点入栈*/
else {
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
}
/*返回遍历结果*/
return res;
}
/**
* 层序遍历
*
* @param root 根节点
* @return 遍历结果
*/
public List<List<Integer>> levelOrder(TreeNode root) {
/*存储遍历结果的列表*/
List<List<Integer>> res = new ArrayList<>();
/*如果根节点为空,直接返回空列表*/
if (root == null) {
return res;
}
/*使用队列来辅助遍历*/
Queue<TreeNode> queue = new LinkedList<>();
/*先将根节点入队*/
queue.offer(root);
/*只要队列不为空,就继续遍历*/
while (!queue.isEmpty()) {
/*当前层的节点数*/
int levelSize = queue.size();
/*存储当前层的节点值*/
List<Integer> level = new ArrayList<>();
/*遍历当前层的所有节点*/
for (int i = 0; i < levelSize; i++) {
/*取出队首节点*/
TreeNode node = queue.poll();
/*将节点的值加入当前层的节点值列表*/
level.add(node.val);
/*如果左子节点不为空,将其入队*/
if (node.left != null) {
queue.offer(node.left);
}
/*如果右子节点不为空,将其入队*/
if (node.right != null) {
queue.offer(node.right);
}
}
/*将当前层的节点值列表加入遍历结果列表*/
res.add(level);
}
/*返回遍历结果*/
return res;
}
public static void main(String[] args) {
/*创建一个二叉树*/
TreeNode root = new TreeNode(3);
TreeNode node1 = new TreeNode(9);
TreeNode node2 = new TreeNode(20);
TreeNode node3 = new TreeNode(15);
TreeNode node4 = new TreeNode(7);
root.left = node1;
root.right = node2;
node2.left = node3;
node2.right = node4;
/*层序遍历二叉树*/
List<List<Integer>> res = new BinaryTreeTraversal().levelOrder(root);
/*输出遍历结果 [[3], [9, 20], [15, 7]]*/
System.out.println(res);
}
}
2、二叉树的最大深度
题目描述:给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
3
/ \
9 20
/ \
15 7
示例:给定二叉树 [3,9,20,null,null,15,7],返回它的最大深度 3 。
该题在 LeetCode 上的题目编号为 104。
解题思路
二叉树的最大深度问题可以使用递归的思想来解决。假设要求以节点 node 为根节点的二叉树的最大深度,可以先分别求出其左子树和右子树的最大深度,然后将二者的最大值加上1,即可得到以 node 为根节点的二叉树的最大深度。因此可以写出递归函数 maxDepth(root),表示求以 root 为根节点的二叉树的最大深度,具体实现如下:
- 如果 root 为空,返回 0。
- 否则,分别求出以 root.left 为根节点的二叉树的最大深度 leftDepth 和以 root.right 为根节点的二叉树的最大深度 rightDepth。
- 返回 leftDepth 和 rightDepth 中的最大值加上1。
递归函数的实现非常简单,可以直接按照上述思路进行编写。时间复杂度为 $O(n)$,其中 $n$ 是二叉树的节点个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
* @date 2023/4/11 00:00
*/
public class MaxDepthTree {
/**
* 具体实现如下:
* <p>
* 如果 root 为空,返回 0。
* 否则,分别求出以 root.left 为根节点的二叉树的最大深度 leftDepth 和以 root.right 为根节点的二叉树的最大深度 rightDepth。
* 返回 leftDepth 和 rightDepth 中的最大值加上1。
*/
public int maxDepth(TreeNode root) {
/*如果根节点为空,返回深度 0*/
if (root == null) {
return 0;
}
/*分别求出左子树和右子树的最大深度*/
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
/*返回左右子树中的最大深度加上1*/
return Math.max(leftDepth, rightDepth) + 1;
}
public static void main(String[] args) {
TreeNode root = new TreeNode(3);
TreeNode node1 = new TreeNode(9);
TreeNode node2 = new TreeNode(20);
TreeNode node3 = new TreeNode(15);
TreeNode node4 = new TreeNode(7);
root.left = node1;
root.right = node2;
node2.left = node3;
node2.right = node4;
int maxDepth = new MaxDepthTree().maxDepth(root);
System.out.println("二叉树的最大深度为:" + maxDepth);
}
}
3、二叉树的最小深度
题目描述:给定一棵二叉树,求其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意,叶子节点是指没有子节点的节点。
例如,给定二叉树 [3,9,20,null,null,15,7],它的最小深度为 2,因为根节点到叶子节点的最短路径为 3->9,所以最小深度为 2。
解题思路
二叉树的最小深度问题可以使用递归的思想来解决。类似于求最大深度,假设要求以节点 node 为根节点的二叉树的最小深度,可以先分别求出其左子树和右子树的最小深度,然后将二者的最小值加上1,即可得到以 node 为根节点的二叉树的最小深度。
但是需要注意的是,当某个节点的左子树或右子树为空时,不能直接返回0,而应该返回非空子树的最小深度。
因此可以写出递归函数 minDepth(root),表示求以 root 为根节点的二叉树的最小深度,具体实现如下:
- 如果 root 为空,返回0。
- 如果 root 的左子树或右子树为空,则返回非空子树的最小深度加1。
- 如果 root 的左子树和右子树均不为空,则分别求出以 root.left 为根节点的二叉树的最小深度 leftDepth 和以 root.right 为根节点的二叉树的最小深度 rightDepth,然后返回二者的最小值加上1。
递归函数的实现非常简单,可以直接按照上述思路进行编写。时间复杂度为 $O(n)$,其中 $n$ 是二叉树的节点个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一棵二叉树,求其最小深度。
* 最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
* 注意,叶子节点是指没有子节点的节点。
* @date 2023/4/11 23:06
*/
public class MinDepthTree {
/**
* 二叉树的最小深度问题可以使用递归的思想来解决。
* 类似于求最大深度,假设要求以节点 node 为根节点的二叉树的最小深度,
* 可以先分别求出其左子树和右子树的最小深度,
* 然后将二者的最小值加上1,即可得到以 node 为根节点的二叉树的最小深度。
*/
public int minDepth(TreeNode root) {
if (root == null) {
/*当前节点为空,返回深度 0*/
return 0;
}
if (root.left == null) {
/*左子树为空,返回右子树的最小深度*/
return minDepth(root.right) + 1;
}
if (root.right == null) {
/*右子树为空,返回左子树的最小深度*/
return minDepth(root.left) + 1;
}
/*当前节点左右子树都不为空,返回左右子树深度的最小值加1*/
return Math.min(minDepth(root.left), minDepth(root.right)) + 1;
}
public static void main(String[] args) {
/*
* 3
* / \
* 9 20
* / \
* 15 7
*/
TreeNode root = new TreeNode(3);
root.left = new TreeNode(9);
root.right = new TreeNode(20);
root.right.left = new TreeNode(15);
root.right.right = new TreeNode(7);
/*输出 2*/
System.out.println(new MinDepthTree().minDepth(root));
}
}
4、对称二叉树
题目描述:给定一棵二叉树,判断它是否是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
例如,下面这棵二叉树是对称的。
而下面这棵二叉树则不是镜像对称的:
原题链接:力扣
解题思路
判断一棵二叉树是否为对称二叉树的最优解法是递归。可以通过比较二叉树的左右子树是否对称来判断整棵二叉树是否对称。具体实现步骤如下:
定义递归函数 isSymmetric(left, right),表示判断以 left 和 right 为根节点的两棵二叉树是否对称。
- 如果 left 和 right 都为空,则返回 true。
- 如果 left 和 right 中有且只有一个为空,则返回 false。
- 如果 left 和 right 的根节点的值不相等,则返回 false。
- 递归判断 left.left 和 right.right 是否对称,递归判断 left.right 和 right.left 是否对称,只有两个都对称时,整棵树才对称。
在主函数中,调用递归函数 isSymmetric(root, root) 判断整棵树是否对称。
时间复杂度是O(n),其中n是二叉树中的节点数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一棵二叉树,判断它是否是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
* @date 2023/4/11 23:13
*/
public class SymmetricTree {
/**
* 判断一棵二叉树是否为对称二叉树的最优解法是递归。可以通过比较二叉树的左右子树是否对称来判断整棵二叉树是否对称。具体实现步骤如下:
* <p>
* 定义递归函数 isSymmetric(left, right),表示判断以 left 和 right 为根节点的两棵二叉树是否对称。
* 如果 left 和 right 都为空,则返回 true。
* 如果 left 和 right 中有且只有一个为空,则返回 false。
* 如果 left 和 right 的根节点的值不相等,则返回 false。
* 递归判断 left.left 和 right.right 是否对称,递归判断 left.right 和 right.left 是否对称,只有两个都对称时,整棵树才对称。
* 在主函数中,调用递归函数 isSymmetric(root, root) 判断整棵树是否对称。
*/
public boolean isSymmetric(TreeNode root) {
/*如果根节点为空,返回true*/
if (root == null) {
return true;
}
/*检查左右子树是否对称*/
return checkSymmetric(root.left, root.right);
}
/*检查左右子树是否对称*/
private boolean checkSymmetric(TreeNode left, TreeNode right) {
/*如果左右子树都为空,返回true*/
if (left == null && right == null) {
return true;
}
/*如果左右子树有一个为空,返回false*/
if (left == null || right == null) {
return false;
}
/*如果左右子树的节点值不相等,返回false*/
if (left.val != right.val) {
return false;
}
/*递归检查左子树的左子树是否与右子树的右子树对称*/
/*以及左子树的右子树是否与右子树的左子树对称*/
return checkSymmetric(left.left, right.right) && checkSymmetric(left.right, right.left);
}
public static void main(String[] args) {
/*创建二叉树*/
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(2);
root.left.left = new TreeNode(3);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(4);
root.right.right = new TreeNode(3);
/*判断是否为对称二叉树*/
boolean result = new SymmetricTree().isSymmetric(root);
/*输出结果*/
if (result) {
System.out.println("是对称二叉树");
} else {
System.out.println("不是对称二叉树");
}
}
}
5、二叉树的最近公共祖先
题目描述:给定一棵二叉树和两个节点,求它们的最近公共祖先。最近公共祖先是指在二叉树中同时拥有给定节点为其子节点的最近的公共祖先节点。
示例:
3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
例如,节点 5 和节点 1 的最近公共祖先是节点 3;节点 5 和节点 4 的最近公共祖先是节点 5。
解题思路
根据最近公共祖先的定义,若节点 p、q 分别在左右子树中,则其最近公共祖先为根节点;若两节点均在左子树中,则继续在左子树中寻找最近公共祖先;若两节点均在右子树中,则继续在右子树中寻找最近公共祖先。因此可以使用递归的方式来实现。
递归函数的具体实现方式如下:
- 如果树为空,返回 null。
- 如果节点 p 或节点 q 与当前节点重合,则说明当前节点是 p 和 q 的最近公共祖先,返回当前节点。
- 在左子树中寻找 p 和 q 的最近公共祖先,得到变量 left。
- 在右子树中寻找 p 和 q 的最近公共祖先,得到变量 right。
- 如果 left 和 right 都为空,则说明 p 和 q 不在树中,返回 null。
- 如果 left 和 right 都不为空,则说明 p 和 q 分别在当前节点的左右子树中,当前节点即为最近公共祖先,返回当前节点。
- 如果 left 为空,则说明 p 和 q 均不在当前节点的左子树中,返回 right。
- 如果 right 为空,则说明 p 和 q 均不在当前节点的右子树中,返回 left。
时间复杂度为 $O(n)$,其中 $n$ 是二叉树的节点个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description
* @date 2023/4/11 23:17
*/
public class CommonAncestor {
/**
* 根据最近公共祖先的定义,若节点 p、q 分别在左右子树中,则其最近公共祖先为根节点;
* 若两节点均在左子树中,则继续在左子树中寻找最近公共祖先;
* 若两节点均在右子树中,则继续在右子树中寻找最近公共祖先。
* 因此可以使用递归的方式来实现。
*/
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
/*如果树为空,返回 null*/
if (root == null) {
return null;
}
/*如果节点 p 或节点 q 与当前节点重合,则说明当前节点是 p 和 q 的最近公共祖先,返回当前节点*/
if (root == p || root == q) {
return root;
}
/*在左子树中寻找 p 和 q 的最近公共祖先,得到变量 left*/
TreeNode left = lowestCommonAncestor(root.left, p, q);
/*在右子树中寻找 p 和 q 的最近公共祖先,得到变量 right*/
TreeNode right = lowestCommonAncestor(root.right, p, q);
/*如果 left 和 right 都为空,则说明 p 和 q 不在树中,返回 null*/
if (left == null && right == null) {
return null;
}
/*如果 left 和 right 都不为空,则说明 p 和 q 分别在当前节点的左右子树中,当前节点即为最近公共祖先,返回当前节点*/
if (left != null && right != null) {
return root;
}
/*如果 left 为空,则说明 p 和 q 均不在当前节点的左子树中,返回 right*/
if (left == null) {
return right;
}
/*如果 right 为空,则说明 p 和 q 均不在当前节点的右子树中,返回 left*/
return left;
}
public static void main(String[] args) {
TreeNode root = new TreeNode(3);
root.left = new TreeNode(5);
root.right = new TreeNode(1);
root.left.left = new TreeNode(6);
root.left.right = new TreeNode(2);
root.right.left = new TreeNode(0);
root.right.right = new TreeNode(8);
root.left.right.left = new TreeNode(7);
root.left.right.right = new TreeNode(4);
TreeNode p = root.left;
TreeNode q = root.right;
TreeNode lca = new CommonAncestor().lowestCommonAncestor(root, p, q);
System.out.println("p=" + p.val + ", q=" + q.val + ", LCA=" + lca.val);
p = root.left;
q = root.left.right.right;
lca = new CommonAncestor().lowestCommonAncestor(root, p, q);
System.out.println("p=" + p.val + ", q=" + q.val + ", LCA=" + lca.val);
}
}
6、二叉树的直径
题目描述:给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
注意:两结点之间的路径长度是以它们之间边的数目表示。
例如:给定二叉树
1
/ \
2 3
/ \
4 5
返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。
解题思路
对于每个节点,其最长路径可以表示为其左子树的最大深度加上右子树的最大深度。因此可以通过递归的方式,计算每个节点的最长路径,取其中的最大值作为整个二叉树的最长路径。
递归函数的具体实现方式如下:
- 如果当前节点为空,返回 0。
- 计算当前节点的左子树的最大深度,得到变量 left。
- 计算当前节点的右子树的最大深度,得到变量 right。
- 更新当前节点的最长路径,取 left + right 的最大值。
- 返回当前节点的最大深度,即 left 和 right 中的最大值加上 1。
时间复杂度为 $O(n)$,其中 $n$ 是二叉树的节点个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 定一棵二叉树,你需要计算它的直径长度。
* 一棵二叉树的直径长度是任意两个结点路径长度中的最大值。
* 这条路径可能穿过也可能不穿过根结点。
* @date 2023/4/11 23:21
*/
public class DiameterOfTree {
/*记录二叉树的最长路径*/
private int maxPath = 0;
/**
* 对于每个节点,其最长路径可以表示为其左子树的最大深度加上右子树的最大深度。
* 因此可以通过递归的方式,计算每个节点的最长路径,取其中的最大值作为整个二叉树的最长路径。
*/
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return maxPath;
}
private int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
/*计算左子树的最大深度*/
int left = maxDepth(root.left);
/*计算右子树的最大深度*/
int right = maxDepth(root.right);
/*更新最长路径*/
maxPath = Math.max(maxPath, left + right);
/*返回当前节点的最大深度*/
return Math.max(left, right) + 1;
}
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
int diameter = new DiameterOfTree().diameterOfBinaryTree(root);
System.out.println("Diameter of the binary tree: " + diameter);
}
}
7、二叉树的路径和
题目描述:给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于给定的目标和。
示例:
来源:力扣(LeetCode)
链接:力扣
解题思路
对于一条从根节点到叶子节点的路径,可以通过递归的方式,依次计算从根节点到当前节点的路径和,并将其与目标值进行比较,若相等,则说明找到了一条符合条件的路径。同时,在递归过程中可以将符合条件的路径数量进行累加。
递归函数的具体实现方式如下:
- 如果当前节点为空,返回 0。
- 计算从根节点到当前节点的路径和,即 sum 减去当前节点的值,得到变量 pathSum。
- 如果当前节点为叶子节点,并且 pathSum 等于当前节点的值,说明找到了一条符合条件的路径,返回 1。
- 递归计算当前节点的左子树中符合条件的路径数量,得到变量 left。
- 递归计算当前节点的右子树中符合条件的路径数量,得到变量 right。
- 返回 left 和 right 的和,即为整个二叉树中符合条件的路径数量。
时间复杂度为O(n),其中n是二叉树的节点个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.ArrayList;
import java.util.List;
/**
* @author yanfengzhang
* @description 给定一个二叉树和一个目标和,
* 判断该树中是否存在根节点到叶子节点的路径,
* 这条路径上所有节点值相加等于给定的目标和。
* @date 2023/4/11 23:27
*/
public class PathSumOfTree {
/**
* 输出二叉树中从根节点到叶子节点路径上所有节点值之和等于目标值的路径
* 我们可以使用深度优先搜索(DFS)来遍历二叉树。
* 在遍历过程中,我们需要记录当前路径的节点值之和,并判断是否满足目标值。
* 当遍历到叶子节点时,如果路径的节点值之和等于目标值,则将该路径添加到结果列表中。
*/
public List<List<Integer>> binaryTreePathSum(TreeNode root, int target) {
/*用于存储结果的列表*/
List<List<Integer>> result = new ArrayList<>();
/*用于存储当前路径的节点值*/
List<Integer> path = new ArrayList<>();
dfs(root, target, path, result);
return result;
}
private void dfs(TreeNode node, int target, List<Integer> path, List<List<Integer>> result) {
if (node == null) {
return;
}
/*将当前节点的值添加到路径中*/
path.add(node.val);
/*到达叶子节点并且路径和等于目标值时,将该路径加入结果列表*/
if (node.left == null && node.right == null && node.val == target) {
result.add(new ArrayList<>(path));
}
/*递归遍历左子树和右子树*/
dfs(node.left, target - node.val, path, result);
dfs(node.right, target - node.val, path, result);
/*回溯,将当前节点从路径中删除*/
path.remove(path.size() - 1);
}
/**
* 计算二叉树中从根节点到叶子节点路径上所有节点值之和等于目标值的路径数量
*
* @param root 二叉树的根节点
* @param sum 目标值
* @return 符合条件的路径数量
*/
public int pathsOfSum(TreeNode root, int sum) {
/*如果当前节点为空,返回 0*/
if (root == null) {
return 0;
}
/*计算当前节点左子树中符合条件的路径数量*/
int left = pathsOfSum(root.left, sum - root.val);
/*计算当前节点右子树中符合条件的路径数量*/
int right = pathsOfSum(root.right, sum - root.val);
/*如果当前节点的值等于目标值,说明当前节点可以作为一条符合条件的路径的起点,此时返回 1,否则返回 0*/
return (sum == root.val ? 1 : 0) + left + right;
}
public static void main(String[] args) {
/* 创建一个二叉树作为示例*/
TreeNode root = new TreeNode(5);
root.left = new TreeNode(4);
root.right = new TreeNode(8);
root.left.left = new TreeNode(11);
root.left.left.left = new TreeNode(7);
root.left.left.right = new TreeNode(2);
root.right.left = new TreeNode(13);
root.right.right = new TreeNode(4);
root.right.right.left = new TreeNode(5);
root.right.right.right = new TreeNode(1);
int targetSum = 22;
int pathCount = new PathSumOfTree().pathsOfSum(root, targetSum);
/*Path count: 2*/
System.out.println("Path count: " + pathCount);
List<List<Integer>> paths = new PathSumOfTree().binaryTreePathSum(root, targetSum);
/*输出结果*/
for (List<Integer> path : paths) {
System.out.println(path);
}
}
}
8、二叉搜索树中的插入操作
题目描述:给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。返回插入后二叉搜索树的根节点。输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。你可以返回任意有效的结果。
示例 1:输入: root = [4,2,7,1,3], val = 5 输出: [4,2,7,1,3,5]
示例 3:输入: root = [40,20,60,10,30,50,70], val = 25 输出: [40,20,60,10,30,50,70,null,null,25]
示例 3:输入: root = [4,2,7,1,3,null,null,null,null,null,null], val = 5 输出: [4,2,7,1,3,5]
提示:
- 给定的树上的节点数介于 0 和 10^4 之间
- 每个节点都有一个唯一整数值,取值范围从 0 到 10^8
- -10^8 <= val <= 10^8
- 新值和原始二叉搜索树中的任意节点值都不同
解题思路
对于二叉搜索树中的插入操作,可以利用其性质,即左子树节点值均小于根节点值,右子树节点值均大于根节点值,来确定插入位置。
具体实现方式如下:
- 从根节点开始,判断插入值与当前节点值的大小关系。
- 如果插入值小于当前节点值,并且当前节点左子树为空,则将插入值作为当前节点的左子节点。
- 如果插入值大于当前节点值,并且当前节点右子树为空,则将插入值作为当前节点的右子节点。
- 如果插入值小于当前节点值,并且当前节点左子树非空,则以当前节点的左子节点为根节点,递归执行步骤1。
- 如果插入值大于当前节点值,并且当前节点右子树非空,则以当前节点的右子节点为根节点,递归执行步骤1。
- 最终返回根节点即可。
时间复杂度为O(h),其中h是树的高度。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。
* 返回插入后二叉搜索树的根节点。输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
* @date 2023/4/11 23:31
*/
public class InsertBST {
/**
* 从根节点开始,判断插入值与当前节点值的大小关系。
* 如果插入值小于当前节点值,并且当前节点左子树为空,则将插入值作为当前节点的左子节点。
* 如果插入值大于当前节点值,并且当前节点右子树为空,则将插入值作为当前节点的右子节点。
* 如果插入值小于当前节点值,并且当前节点左子树非空,则以当前节点的左子节点为根节点,递归执行步骤1。
* 如果插入值大于当前节点值,并且当前节点右子树非空,则以当前节点的右子节点为根节点,递归执行步骤1。
* 最终返回根节点即可。
*/
public TreeNode insertIntoBST(TreeNode root, int val) {
/*如果根节点为空,则创建一个新节点作为根节点并返回*/
if (root == null) {
return new TreeNode(val);
}
/*如果插入值小于当前节点值,则插入到左子树中*/
if (val < root.val) {
root.left = insertIntoBST(root.left, val);
}
/*如果插入值大于等于当前节点值,则插入到右子树中*/
else {
root.right = insertIntoBST(root.right, val);
}
/*返回插入后的根节点*/
return root;
}
public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(2);
root.right = new TreeNode(7);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(3);
TreeNode newRoot = new InsertBST().insertIntoBST(root, 5);
/*4*/
System.out.println(newRoot.val);
/*7*/
System.out.println(newRoot.right.val);
/*5*/
System.out.println(newRoot.right.left.val);
}
}
9、二叉搜索树中的删除操作
题目描述:给定一个二叉搜索树(Binary Search Tree),删除其中的一个节点,并保证删除后的树仍然是一棵二叉搜索树。
若要删除的节点包含两个子节点,应该先找到右子树的最小节点,将其替换到要删除的节点上,再删除右子树的最小节点。
示例 1:输入: root = [5,3,6,2,4,null,7], key = 3 输出: [5,4,6,2,null,null,7]
解释: 给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后将它删除。由于它有两个子节点,我们需要先找到它右子树上最小的节点,即节点值为 4 的节点,然后将它替换到要删除的节点上。
示例 2:输入: root = [5,3,6,2,4,null,7], key = 0 输出: [5,3,6,2,4,null,7]
解释: 树中不存在值为 0 的节点,所以我们不需要删除任何东西。
示例 3:输入: root = [], key = 0 输出: []
解题思路
二叉搜索树是一棵有序的树,对于一个节点,其左子树中的节点都比它小,右子树中的节点都比它大。因此,删除操作需要考虑三种情况:
- 要删除的节点没有子节点,直接删除即可;
- 要删除的节点只有一个子节点,将其子节点上移,然后删除该节点;
- 要删除的节点有两个子节点,需要找到其右子树上的最小节点,将该节点替换到要删除的节点上,然后删除该节点。
为了方便操作,我们可以写一个递归函数 removeNode(node, key) 来删除节点。其中,node 表示当前节点,key 表示要删除的节点值。
对于当前节点 node,如果它等于 key,则需要删除它。此时,我们需要根据节点子树的情况进行分类讨论:
- 如果 node 没有左子节点和右子节点,直接返回 null 即可;
- 如果 node 没有左子节点,则直接返回其右子节点,用其右子节点替换 node 即可;
- 如果 node 没有右子节点,则直接返回其左子节点,用其左子节点替换 node 即可;
- 如果 node 同时拥有左右子节点,则需要找到其右子树上的最小节点 minNode,用 minNode 替换 node,然后将 minNode 删除即可。
时间复杂度:删除操作的时间复杂度为 O(h),其中 h 是二叉搜索树的高度。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一个二叉搜索树(Binary Search Tree),
* 删除其中的一个节点,并保证删除后的树仍然是一棵二叉搜索树。
* @date 2023/4/11 23:36
*/
public class DeleteBST {
/**
* 二叉搜索树是一棵有序的树,对于一个节点,其左子树中的节点都比它小,右子树中的节点都比它大。因此,删除操作需要考虑三种情况:
* <p>
* 要删除的节点没有子节点,直接删除即可;
* 要删除的节点只有一个子节点,将其子节点上移,然后删除该节点;
* 要删除的节点有两个子节点,需要找到其右子树上的最小节点,将该节点替换到要删除的节点上,然后删除该节点。
* 为了方便操作,我们可以写一个递归函数 removeNode(node, key) 来删除节点。其中,node 表示当前节点,key 表示要删除的节点值。
* <p>
* 对于当前节点 node,如果它等于 key,则需要删除它。此时,我们需要根据节点子树的情况进行分类讨论:
* <p>
* 如果 node 没有左子节点和右子节点,直接返回 null 即可;
* 如果 node 没有左子节点,则直接返回其右子节点,用其右子节点替换 node 即可;
* 如果 node 没有右子节点,则直接返回其左子节点,用其左子节点替换 node 即可;
* 如果 node 同时拥有左右子节点,则需要找到其右子树上的最小节点 minNode,用 minNode 替换 node,然后将 minNode 删除即可。
* 时间复杂度:删除操作的时间复杂度为 O(h),其中 h 是二叉搜索树的高度。
*/
public TreeNode deleteNode(TreeNode root, int key) {
/*1. 如果当前节点为空,则返回空节点*/
if (root == null) {
return null;
}
/*2. 如果要删除的节点值小于当前节点值,则在左子树中进行删除操作*/
if (key < root.val) {
root.left = deleteNode(root.left, key);
}
/*3. 如果要删除的节点值大于当前节点值,则在右子树中进行删除操作*/
else if (key > root.val) {
root.right = deleteNode(root.right, key);
}
/*4. 如果要删除的节点就是当前节点*/
else {
/*4.1 如果当前节点没有左右子树,则直接删除该节点*/
if (root.left == null && root.right == null) {
root = null;
/*4.2 如果当前节点有右子树,则将右子树中最小的节点值赋值给当前节点,并在右子树中删除该节点*/
} else if (root.right != null) {
root.val = findMin(root.right);
root.right = deleteNode(root.right, root.val);
}
/*4.3 如果当前节点只有左子树,则将左子树中最大的节点值赋值给当前节点,并在左子树中删除该节点*/
else {
root.val = findMax(root.left);
root.left = deleteNode(root.left, root.val);
}
}
/*5. 返回删除操作后的二叉搜索树根节点*/
return root;
}
/*找到二叉搜索树中最小的节点值*/
private int findMin(TreeNode node) {
while (node.left != null) {
node = node.left;
}
return node.val;
}
/*找到二叉搜索树中最大的节点值*/
private int findMax(TreeNode node) {
while (node.right != null) {
node = node.right;
}
return node.val;
}
public static void main(String[] args) {
/*构造二叉搜索树*/
TreeNode root = new TreeNode(5);
root.left = new TreeNode(3);
root.right = new TreeNode(6);
root.left.left = new TreeNode(2);
root.left.right = new TreeNode(4);
root.right.right = new TreeNode(7);
/*删除值为3的节点*/
TreeNode newRoot = new DeleteBST().deleteNode(root, 3);
}
}
10、二叉搜索树中的搜索操作
题目描述:给定一个二叉搜索树(Binary Search Tree, BST)和一个值。你需要在BST中找到节点值等于给定值的节点。返回以该节点作为根的子树。如果节点不存在,则返回 NULL。
示例:
输入:给定二叉搜索树:
4
/ \
2 7
/ \
1 3
和值: 2
输出:
2
/ \1 3
解题思路
第一种思路:通过迭代的方式从根节点开始遍历二叉搜索树,如果当前节点为 null 或者值等于目标值,则返回当前节点。否则,如果当前节点的值小于目标值,则在其右子树中继续查找;如果当前节点的值大于目标值,则在其左子树中继续查找。这样,如果二叉搜索树中存在目标值,就可以找到它并返回对应的节点,如果不存在,则返回 null。
第二种思路:通过递归的方式从根节点开始遍历二叉搜索树,如果当前节点为 null 或者值等于目标值,则返回当前节点。否则,如果当前节点的值小于目标值,则在其右子树中继续查找;如果当前节点的值大于目标值,则在其左子树中继续查找。这样,如果二叉搜索树中存在目标值,就可以找到它并返回对应的节点,如果不存在,则返回 null。
这两种思路的代码实现比较简单,时间复杂度都是 O(h),其中 h 是二叉搜索树的高度。在最坏情况下,当二叉搜索树退化成链表时,时间复杂度为 O(n),其中 n 是二叉搜索树的节点数。在平均情况下,时间复杂度为 O(logn),其中 n 是二叉搜索树的节点数。因此,这两种思路都是比较高效的二叉搜索树搜索算法。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一个二叉搜索树(Binary Search Tree, BST)和一个值。
* 你需要在BST中找到节点值等于给定值的节点。
* 返回以该节点作为根的子树。如果节点不存在,则返回NULL。
* @date 2023/4/12 23:57
*/
public class SearchBST {
/**
* 通过递归的方式从根节点开始遍历二叉搜索树,
* 如果当前节点为 null 或者值等于目标值,则返回当前节点。
* 否则,如果当前节点的值小于目标值,则在其右子树中继续查找;
* 如果当前节点的值大于目标值,则在其左子树中继续查找。
* 这样,如果二叉搜索树中存在目标值,就可以找到它并返回对应的节点,如果不存在,则返回 null。
*/
public boolean searchBST1(TreeNode root, int val) {
if (root == null) {
return false;
}
if (root.val == val) {
return true;
}
if (root.val > val) {
return searchBST1(root.left, val);
}
return searchBST1(root.right, val);
}
/**
* 通过迭代的方式从根节点开始遍历二叉搜索树,
* 如果当前节点为 null 或者值等于目标值,则返回当前节点。
* 否则,如果当前节点的值小于目标值,则在其右子树中继续查找;
* 如果当前节点的值大于目标值,则在其左子树中继续查找。
* 这样,如果二叉搜索树中存在目标值,就可以找到它并返回对应的节点,如果不存在,则返回 null。
*/
public boolean searchBST2(TreeNode root, int val) {
while (root != null) {
if (root.val == val) {
return true;
} else if (root.val > val) {
root = root.left;
} else {
root = root.right;
}
}
return false;
}
public static void main(String[] args) {
/*构建一棵二叉搜索树*/
TreeNode root = new TreeNode(5);
root.left = new TreeNode(3);
root.right = new TreeNode(7);
root.left.left = new TreeNode(2);
root.left.right = new TreeNode(4);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(8);
/*测试 searchBST1 方法*/
/*true*/
System.out.println(new SearchBST().searchBST1(root, 6));
/*false*/
System.out.println(new SearchBST().searchBST1(root, 9));
/*测试 searchBST2 方法*/
/*true*/
System.out.println(new SearchBST().searchBST2(root, 6));
/*false*/
System.out.println(new SearchBST().searchBST2(root, 9));
}
}
11、二叉树的层平均值
题目描述:给定一个非空二叉树的根节点
root
, 以数组的形式返回每一层节点的平均值。与实际答案相差10-5
以内的答案可以被接受。提示:
- 树中节点数量在
[1, 104]
范围内-231 <= Node.val <= 231 - 1
原题目的链接:
解题思路
使用广度优先搜索(BFS)遍历二叉树,对于每一层节点,计算它们的平均值并将其加入结果数组中。
具体实现方式:
- 首先,定义一个队列,将根节点入队。
- 然后,每次遍历队列中的元素,并将它们的子节点入队。
- 对于每一层的节点,统计它们的平均值,并将其加入结果数组中。
- 最后,返回结果数组。
时间复杂度为O(n),其中 n 是二叉树的节点个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* @author yanfengzhang
* @description 给定一个非空二叉树的根节点root,
* 以数组的形式返回每一层节点的平均值。与实际答案相差10-5以内的答案可以被接受。
* @date 2023/4/12 23:39
*/
public class AverageLevels {
/**
* 使用广度优先搜索(BFS)遍历二叉树,对于每一层节点,计算它们的平均值并将其加入结果数组中。
*
* 具体实现方式:
* 首先,定义一个队列,将根节点入队。
* 然后,每次遍历队列中的元素,并将它们的子节点入队。
* 对于每一层的节点,统计它们的平均值,并将其加入结果数组中。
* 最后,返回结果数组。
*/
public List<Double> averageOfLevels(TreeNode root) {
List<Double> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
/*当前层的节点数*/
int levelSize = queue.size();
/*当前层节点值的和*/
double levelSum = 0;
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
levelSum += node.val;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
double levelAvg = levelSum / levelSize;
result.add(levelAvg);
}
return result;
}
public static void main(String[] args) {
/*构造二叉树*/
TreeNode root = new TreeNode(3);
TreeNode node1 = new TreeNode(9);
TreeNode node2 = new TreeNode(20);
TreeNode node3 = new TreeNode(15);
TreeNode node4 = new TreeNode(7);
root.left = node1;
root.right = node2;
node2.left = node3;
node2.right = node4;
/*计算每层节点值的平均数*/
List<Double> result = new AverageLevels().averageOfLevels(root);
/*输出结果*/
/*预期输出 [3.0, 14.5, 11.0]*/
System.out.println(result);
}
}
12、二叉树的镜像
题目描述:请完成一个函数,输入一个二叉树,该函数输出它的镜像。
限制:0 <= 节点个数 <= 1000
注意:本题力扣与主站 226 题相同:https://leetcode-cn.com/problems/invert-binary-tree/
来源:力扣(LeetCode)
解题思路
二叉树的镜像可以通过递归来实现。具体地,我们定义一个递归函数 mirror,交换当前节点的左右子树,然后分别递归地交换左右子树。
时间复杂度:O(n),其中 n 是二叉树中的节点个数,每个节点在递归中只被访问一次。
空间复杂度:O(n),最坏情况下,二叉树退化为链表,系统使用 O(n) 大小的栈空间。
当二叉树特别大时,递归方法可能会导致栈溢出。在这种情况下,使用非递归的方法来实现二叉树的镜像更为稳妥。非递归方法可以通过使用栈来模拟递归的过程,从而避免栈溢出的问题。具体实现可以使用迭代的方式,从根节点开始,将每个节点的左右子节点交换,同时将左右子节点入栈,然后取出栈顶节点,再将其左右子节点交换并入栈,如此循环直到栈为空。
具体代码展示
递归方式下示例:
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
/**
* @author yanfengzhang
* @description 请完成一个函数,输入一个二叉树,该函数输出它的镜像。
* @date 2023/4/12 23:48
*/
public class MirrorTree {
/**
* 递归交换每个节点的左右子树
* <p>
* 递归函数 mirrorTree 接收一个树节点 root 作为参数,返回将该节点为根的子树转化为镜像后的根节点。
* 首先判断根节点是否为空,若为空则直接返回 null。
* 否则,我们先将根节点的左右子树分别保存在 left 和 right 中。
* 然后将根节点的左子树转化为镜像后作为新的右子树,右子树转化为镜像后作为新的左子树。
* 最后返回根节点即可。
* 该算法的时间复杂度和空间复杂度均为 $O(n)$,其中 $n$ 是树中节点的个数。
*
* @param root 给定的树的根节点
* @return 返回镜像后的树的根节点
*/
public TreeNode mirrorTree(TreeNode root) {
/*如果根节点为空,直接返回 null*/
if (root == null) {
return null;
}
/*保存当前节点的左子树,作为递归后的右子树*/
TreeNode left = root.left;
/*递归交换当前节点的左右子树*/
root.left = mirrorTree(root.right);
root.right = mirrorTree(left);
/*返回镜像后的根节点*/
return root;
}
public static void main(String[] args) {
Integer[] nums = {4, 2, 7, 1, 3, 6, 9};
TreeNode root = createTree(nums);
TreeNode newRoot = new MirrorTree().mirrorTree(root);
/*验证镜像是否正确*/
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
queue.offer(newRoot);
while (!queue.isEmpty()) {
TreeNode node1 = queue.poll();
TreeNode node2 = queue.poll();
if (node1 == null && node2 == null) {
continue;
}
if (node1 == null || node2 == null || node1.val != node2.val) {
System.out.println("Test failed.");
return;
}
queue.offer(node1.left);
queue.offer(node2.left);
queue.offer(node1.right);
queue.offer(node2.right);
}
System.out.println("Test passed.");
}
public static TreeNode createTree(Integer[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
Queue<TreeNode> queue = new LinkedList<>();
TreeNode root = new TreeNode(nums[0]);
queue.offer(root);
int i = 1;
while (i < nums.length) {
TreeNode node = queue.poll();
if (node == null) {
continue;
}
Integer leftVal = nums[i++];
Integer rightVal = i < nums.length ? nums[i++] : null;
node.left = leftVal != null ? new TreeNode(leftVal) : null;
node.right = rightVal != null ? new TreeNode(rightVal) : null;
queue.offer(node.left);
queue.offer(node.right);
}
return root;
}
}
以下是非递归实现二叉树镜像的代码示例:
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
/*创建一个栈,用于存放待翻转的节点*/
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
/*取出栈顶元素*/
TreeNode node = stack.pop();
/*翻转当前节点的左右子树*/
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
/*将左右子节点非空的节点入栈*/
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
}
return root;
}
13、二叉树的序列化与反序列化
题目描述:序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。
提示:
- 树中结点数在范围
[0, 104]
内-1000 <= Node.val <= 1000
来源:力扣(LeetCode)
链接:力扣
解题思路
序列化:采用前序遍历(根-左-右)进行序列化,如果遇到 null 节点,则用特殊字符 # 代替。
反序列化:将字符串按逗号分割成数组,然后再用递归的方法反序列化。
时间复杂度:
- 序列化:O(n),其中 n 是二叉树中的节点数。对每个节点访问一次。
- 反序列化:O(n),其中 n 是二叉树中的节点数。对每个节点访问一次。
空间复杂度:
- 序列化:O(n),其中 n 是二叉树中的节点数。空间复杂度取决于递归调用的栈空间。
- 反序列化:O(n),其中 n 是二叉树中的节点数。空间复杂度取决于递归调用的栈空间。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 序列化是将一个数据结构或者对象转换为连续的比特位的操作,
* 进而可以将转换后的数据存储在一个文件或者内存中,
* 同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
* <p>
* 请设计一个算法来实现二叉树的序列化与反序列化。
* 这里不限定你的序列 / 反序列化算法执行逻辑,
* 你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
* @date 2023/4/12 23:56
*/
public class SerializeTree {
/*8Encodes a tree to a single string.*/
public String serialize(TreeNode root) {
if (root == null) {
return "null";
}
String left = serialize(root.left);
String right = serialize(root.right);
return root.val + "," + left + "," + right;
}
/**
* Decodes your encoded data to tree.
*/
public TreeNode deserialize(String data) {
String[] dataArray = data.split(",");
int[] index = {0};
return deserializeHelper(dataArray, index);
}
private TreeNode deserializeHelper(String[] dataArray, int[] index) {
if (dataArray[index[0]].equals("null")) {
index[0]++;
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(dataArray[index[0]]));
index[0]++;
root.left = deserializeHelper(dataArray, index);
root.right = deserializeHelper(dataArray, index);
return root;
}
public static void main(String[] args) {
/*构造一个二叉树*/
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.right.left = new TreeNode(4);
root.right.right = new TreeNode(5);
/*序列化二叉树*/
String serialized = new SerializeTree().serialize(root);
System.out.println("Serialized Tree: " + serialized);
/*反序列化二叉树*/
TreeNode deserialized = new SerializeTree().deserialize(serialized);
System.out.println("Deserialized Tree: " + new SerializeTree().serialize(deserialized));
}
}
14、二叉树的右视图
题目描述:给定一棵二叉树,求其从右侧看所能看到的节点值序列,即每层最右边的节点。
例如,给定以下二叉树:
1 <---
/ \
2 3 <---
\ \
5 4 <---
你应该返回 [1, 3, 4]。
原题目描述和链接:力扣
解题思路
二叉树的右视图可以通过层序遍历来实现。在层序遍历时,每层最后一个节点即为该层最右边的节点,因此只需要记录每层的最后一个节点即可。具体实现步骤如下:
- 如果根节点为空,则返回空列表。
- 初始化结果列表res和队列queue,将根节点加入队列。
- 进入循环,每次循环表示遍历一层。先获取当前队列的长度size,将该层最后一个节点的值加入res中。然后依次弹出队列中的节点,并将其左右子节点加入队列中。
- 循环结束后,返回结果列表res。
由于需要遍历整个二叉树,因此时间复杂度为O(n),其中n为二叉树的节点数。由于需要使用队列存储每层的节点,因此空间复杂度为O(w),其中w为二叉树的最大宽度。在最坏情况下,即二叉树为满二叉树时,w的值为n/2,因此空间复杂度为O(n)。
综上所述,层序遍历的方法是求解二叉树右视图问题的最优解法。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* @author yanfengzhang
* @description 给定一棵二叉树,求其从右侧看所能看到的节点值序列,即每层最右边的节点。
* @date 2023/4/12 23:59
*/
public class RightSideView {
/**
* 求二叉树右视图:二叉树的右视图可以通过层序遍历来实现。
* 在层序遍历时,每层最后一个节点即为该层最右边的节点,
* 因此只需要记录每层的最后一个节点即可。具体实现步骤如下:
* <p>
* 如果根节点为空,则返回空列表。
* 初始化结果列表res和队列queue,将根节点加入队列。
* 进入循环,每次循环表示遍历一层。先获取当前队列的长度size,将该层最后一个节点的值加入res中。然后依次弹出队列中的节点,并将其左右子节点加入队列中。
* 循环结束后,返回结果列表res。
*
* @param root 二叉树根节点
* @return 右视图节点值序列
*/
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
if (i == size - 1) {
/*当前层最后一个节点是从右侧能看到的节点*/
result.add(node.val);
}
if (node.left != null) {
/*将左子节点加入队列*/
queue.offer(node.left);
}
if (node.right != null) {
/*将右子节点加入队列*/
queue.offer(node.right);
}
}
}
return result;
}
public static void main(String[] args) {
/*创建一颗二叉树*/
TreeNode root = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
TreeNode node7 = new TreeNode(7);
root.left = node2;
root.right = node3;
node2.right = node5;
node3.right = node4;
node5.left = node6;
node5.right = node7;
/*测试右视图函数*/
List<Integer> res = new RightSideView().rightSideView(root);
System.out.println(res);
}
}
15、二叉树的最大宽度
题目描述:给定一棵二叉树,求其最大宽度(即树的最大层宽)。
树的 最大宽度 是所有层中最大的 宽度 。
每一层的 宽度 被定义为该层最左和最右的非空节点(即,两个端点)之间的长度。将这个二叉树视作与满二叉树结构相同,两端点间会出现一些延伸到这一层的 null 节点,这些 null 节点也计入长度。
题目数据保证答案将会在 32 位 带符号整数范围内。
提示:
- 树中节点的数目范围是
[1, 3000]
-100 <= Node.val <= 100
参考链接:力扣
解题思路
最优解法的思路是基于层序遍历的,需要使用一个队列来存储每一层的节点,同时记录每个节点在该层的位置(可以用编号或者下标来表示),然后计算每一层的宽度,并找到最大的宽度即可。
具体来说,使用一个队列来进行层序遍历,每次遍历一层时,将该层节点加入队列中,并且记录下每个节点的位置,初始时根节点的位置为0,左儿子节点的位置为当前节点的位置乘2,右儿子节点的位置为当前节点的位置乘2加1。然后计算该层节点的宽度,即最后一个节点的位置减去第一个节点的位置再加1,更新最大宽度即可。
需要注意的是,对于一些空节点,我们也需要将其位置记录下来,以便后续计算该层的宽度。
该算法的时间复杂度为O(n),其中n是二叉树中节点的个数,空间复杂度为O(n)。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import javafx.util.Pair;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.LinkedList;
import java.util.Queue;
/**
* @author yanfengzhang
* @description
* @date 2023/4/13 00:03
*/
public class WidthTree {
/**
* 最优解法的思路是基于层序遍历的,
* 需要使用一个队列来存储每一层的节点,
* 同时记录每个节点在该层的位置(可以用编号或者下标来表示),
* 然后计算每一层的宽度,并找到最大的宽度即可。
* <p>
* 具体来说,使用一个队列来进行层序遍历,
* 每次遍历一层时,将该层节点加入队列中,并且记录下每个节点的位置,
* 初始时根节点的位置为0,左儿子节点的位置为当前节点的位置乘2,
* 右儿子节点的位置为当前节点的位置乘2加1。
* 然后计算该层节点的宽度,
* 即最后一个节点的位置减去第一个节点的位置再加1,更新最大宽度即可。
*/
public int widthOfBinaryTree(TreeNode root) {
if (root == null) {
return 0;
}
int maxWidth = 0;
Queue<Pair<TreeNode, Integer>> queue = new LinkedList<>();
/*将根节点入队,根节点的位置为 1*/
queue.offer(new Pair<>(root, 1));
while (!queue.isEmpty()) {
int size = queue.size();
/*记录本层最左侧节点的位置*/
int left = queue.peek().getValue();
for (int i = 0; i < size; i++) {
Pair<TreeNode, Integer> pair = queue.poll();
int pos = pair.getValue();
/*更新最大宽度*/
maxWidth = Math.max(maxWidth, pos - left + 1);
TreeNode node = pair.getKey();
if (node.left != null) {
/*左子节点位置为父节点位置的 2 倍*/
queue.offer(new Pair<>(node.left, pos * 2));
}
if (node.right != null) {
/*右子节点位置为父节点位置的 2 倍加 1*/
queue.offer(new Pair<>(node.right, pos * 2 + 1));
}
}
}
return maxWidth;
}
public static void main(String[] args) {
TreeNode root = new TreeNode(1);
root.left = new TreeNode(3);
root.right = new TreeNode(2);
root.left.left = new TreeNode(5);
root.left.right = new TreeNode(3);
root.right.right = new TreeNode(9);
int maxWidth = new WidthTree().widthOfBinaryTree(root);
/*输出 4*/
System.out.println(maxWidth);
}
}
16、前序遍历和中序遍历构造二叉树
题目描述:给定一棵二叉树的前序遍历和中序遍历,需要构造该二叉树。前序遍历的顺序为:根节点、左子树、右子树;中序遍历的顺序为:左子树、根节点、右子树。
例如,给定如下二叉树的前序遍历和中序遍历:
前序遍历:[3,9,20,15,7]
中序遍历:[9,3,15,20,7]
我们可以先通过前序遍历找到根节点,即3。然后在中序遍历中找到3的位置,3左边的数就是3的左子树的中序遍历,3右边的数就是3的右子树的中序遍历。在前序遍历中,3后面的数就是3的左子树的前序遍历,3右边的数就是3的右子树的前序遍历。因此,我们可以递归地构造出整棵二叉树。
最终,我们得到如下二叉树:
3
/ \
9 20
/ \
15 7
需要注意的是,如果二叉树中存在重复的值,则无法唯一确定二叉树的结构。因此,前序遍历和中序遍历需要同时给出。
解题思路
对于给定的一棵二叉树的前序遍历和中序遍历,我们可以通过递归来构建这棵二叉树。具体的方法如下:
- 从前序遍历中得到根节点的值。
- 在中序遍历中找到根节点的位置,根节点左边的部分是二叉树的左子树,右边的部分是二叉树的右子树。
- 根据左子树和右子树的节点数,可以在前序遍历中确定左子树和右子树的范围。
- 递归构建左子树和右子树,并将左右子树分别连接到根节点上。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
import java.util.HashMap;
import java.util.Map;
/**
* @author yanfengzhang
* @description 给定一棵二叉树的前序遍历和中序遍历,需要构造该二叉树。
* 前序遍历的顺序为:根节点、左子树、右子树;
* 中序遍历的顺序为:左子树、根节点、右子树。
* @date 2023/4/13 00:07
*/
public class BuildTreeByTraver {
/**
* 通过前序遍历和中序遍历构造二叉树
*
* @param preorder 前序遍历序列
* @param inorder 中序遍历序列
* @return 构造完成的二叉树根节点
*/
public TreeNode buildTree(int[] preorder, int[] inorder) {
/*构造中序遍历序列中节点值和对应索引的映射表*/
Map<Integer, Integer> inorderIndexMap = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
inorderIndexMap.put(inorder[i], i);
}
return buildTree(preorder, 0, preorder.length - 1, inorderIndexMap, 0, inorder.length - 1);
}
/**
* 在指定的前序遍历序列范围和中序遍历序列范围内构造二叉树
*
* @param preorder 前序遍历序列
* @param preStart 前序遍历序列的起始索引
* @param preEnd 前序遍历序列的终止索引
* @param inorderIndexMap 中序遍历序列的节点值和索引的映射表
* @param inStart 中序遍历序列的起始索引
* @param inEnd 中序遍历序列的终止索引
* @return 构造完成的二叉树根节点
*/
private TreeNode buildTree(int[] preorder, int preStart, int preEnd, Map<Integer, Integer> inorderIndexMap,
int inStart, int inEnd) {
/*如果前序遍历序列的起始索引大于终止索引,则构造完成*/
if (preStart > preEnd) {
return null;
}
/*前序遍历序列的第一个节点是当前子树的根节点*/
int rootVal = preorder[preStart];
TreeNode root = new TreeNode(rootVal);
/*如果前序遍历序列中仅有一个节点,直接返回当前节点作为根节点*/
if (preStart == preEnd) {
return root;
}
/*在中序遍历序列中找到根节点的索引*/
int rootIndexInorder = inorderIndexMap.get(rootVal);
/*根据中序遍历中根节点的索引,将左右子树分别递归构造*/
int leftSubtreeSize = rootIndexInorder - inStart;
int rightSubtreeSize = inEnd - rootIndexInorder;
TreeNode leftSubtree = buildTree(preorder, preStart + 1, preStart + leftSubtreeSize,
inorderIndexMap, inStart, rootIndexInorder - 1);
TreeNode rightSubtree = buildTree(preorder, preStart + leftSubtreeSize + 1, preEnd,
inorderIndexMap, rootIndexInorder + 1, inEnd);
/*将构造好的左右子树挂到当前根节点上*/
root.left = leftSubtree;
root.right = rightSubtree;
return root;
}
public static void main(String[] args) {
int[] preorder = {1, 2, 4, 7, 3, 5, 6, 8};
int[] inorder = {4, 7, 2, 1, 5, 3, 8, 6};
TreeNode root = new BuildTreeByTraver().buildTree(preorder, inorder);
}
}
17、平衡二叉树
题目描述:给定一个二叉树,判断它是否是高度平衡的二叉树。高度平衡的二叉树定义为:二叉树的每个节点的左右两个子树的高度差不超过1。
题目链接:力扣
解题思路
一种常用的最优解法是通过递归进行深度优先搜索(DFS)。具体思路如下:
- 对于每个节点,分别计算其左右子树的高度。
- 判断当前节点的左右子树高度差是否超过1,如果超过1,则返回 false,表示不是平衡二叉树。
- 如果当前节点的左右子树高度差没有超过1,则继续递归判断其左右子树是否是平衡二叉树。
- 递归的终止条件是当节点为空时,返回 true。
通过这种递归的方式,可以自顶向下地判断每个节点是否满足平衡二叉树的定义,时间复杂度为 O(n),其中 n 是二叉树的节点数量。空间复杂度为 O(h),其中 h 是二叉树的高度,最坏情况下可能是 O(n)。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一个二叉树,判断它是否是高度平衡的二叉树。
* 高度平衡的二叉树定义为:二叉树的每个节点的左右两个子树的高度差不超过1。
* @date 2023/4/13 00:13
*/
public class BalancedTree {
/**
* 一种常用的最优解法是通过递归进行深度优先搜索(DFS)。具体思路如下:
* <p>
* 对于每个节点,分别计算其左右子树的高度。
* 判断当前节点的左右子树高度差是否超过1,如果超过1,则返回 false,表示不是平衡二叉树。
* 如果当前节点的左右子树高度差没有超过1,则继续递归判断其左右子树是否是平衡二叉树。
* 递归的终止条件是当节点为空时,返回 true。
*/
public boolean isBalanced(TreeNode root) {
if (root == null) {
/*空树视为平衡二叉树*/
return true;
}
/*判断左子树和右子树的高度差是否超过1,如果超过1则不是平衡二叉树*/
if (Math.abs(height(root.left) - height(root.right)) > 1) {
return false;
}
/*判断左子树和右子树是否都是平衡二叉树*/
return isBalanced(root.left) && isBalanced(root.right);
}
/**
* 计算二叉树的高度
*/
private int height(TreeNode node) {
if (node == null) {
return 0;
}
/*递归计算左子树和右子树的最大高度,并加上当前节点的高度1*/
return Math.max(height(node.left), height(node.right)) + 1;
}
public static void main(String[] args) {
/*构建一个二叉树*/
TreeNode root = new TreeNode(1);
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
TreeNode node4 = new TreeNode(4);
TreeNode node5 = new TreeNode(5);
TreeNode node6 = new TreeNode(6);
root.left = node2;
root.right = node3;
node2.left = node4;
node2.right = node5;
node3.right = node6;
/*判断根节点是否为平衡二叉树*/
boolean isBalanced = new BalancedTree().isBalanced(root);
/*输出结果*/
if (isBalanced) {
System.out.println("该二叉树是平衡二叉树。");
} else {
System.out.println("该二叉树不是平衡二叉树。");
}
}
}
18、从根到叶的二进制数之和(Sum Root to Leaf Numbers)
题目描述:给定一个二叉树,其中每个节点都包含一个 0 或 1 的值,每条从根到叶子节点的路径都代表一个二进制数。求所有路径代表的二进制数之和。
例如,给定以下二叉树:
1
/ \
2 3
从根到叶子节点的路径包括 1->2 和 1->3,所以二进制数之和为 12 + 13 = 25。
以下是修正后的题目描述:
给定一个二叉树,其中每个节点都包含一个 0 或 1 的值,每条从根到叶子节点的路径都代表一个二进制数。求所有路径代表的二进制数之和。
请编写一个函数来计算二叉树中所有路径代表的二进制数之和。
来源:力扣(LeetCode)
链接:力扣
解题思路
最优解法是通过深度优先搜索(DFS)来遍历二叉树的所有路径,并计算路径代表的二进制数之和。
具体步骤如下:
- 定义一个全局变量 sum,用于存储二进制数之和。
- 从根节点开始,调用递归函数 dfs 进行深度优先搜索。
- 在 dfs 函数中,传入当前节点和当前路径的值。初始时,当前节点为根节点,当前路径的值为0。
- 如果当前节点为叶子节点,将当前路径的值乘以2并加上叶子节点的值,然后将结果累加到 sum 中。
- 如果当前节点不是叶子节点,分别对其左子节点和右子节点调用递归函数 dfs。将当前路径的值乘以2并加上当前节点的值作为新的路径值传递给递归函数。
- 当递归函数返回时,表示已经遍历完所有路径。返回 sum 作为最终结果。
该算法的时间复杂度是 O(N),其中 N 是二叉树中的节点数,因为需要遍历所有节点。空间复杂度是 O(H),其中 H 是二叉树的高度,因为需要在递归调用过程中维护路径值。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一个二叉树,其中每个节点都包含一个 0 或 1 的值,每条从根到叶子节点的路径都代表一个二进制数。求所有路径代表的二进制数之和。
* 例如,给定以下二叉树:
* 1
* / \
* 2 3
* 从根到叶子节点的路径包括 1->2 和 1->3,所以二进制数之和为 12 + 13 = 25。
* 以下是修正后的题目描述:
* 给定一个二叉树,其中每个节点都包含一个 0 或 1 的值,每条从根到叶子节点的路径都代表一个二进制数。求所有路径代表的二进制数之和。
* 请编写一个函数来计算二叉树中所有路径代表的二进制数之和。
* 来源:力扣(LeetCode)
* 链接:https://leetcode-cn.com/problems/sum-root-to-leaf-numbers
* @date 2023/4/12 22:56
*/
public class SumRootToLeafNumbers {
int sum = 0;
public int sumNumbers(TreeNode root) {
if (root == null) {
return 0;
}
dfs(root, 0);
return sum;
}
private void dfs(TreeNode node, int pathValue) {
/*更新路径值*/
pathValue = pathValue * 10 + node.val;
/*到达叶子节点,累加到二进制数之和*/
if (node.left == null && node.right == null) {
sum += pathValue;
return;
}
/*递归遍历左子树和右子树*/
if (node.left != null) {
dfs(node.left, pathValue);
}
if (node.right != null) {
dfs(node.right, pathValue);
}
}
public static void main(String[] args) {
// 创建二叉树
TreeNode root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
// 计算二叉树中所有路径代表的二进制数之和
int sum = new SumRootToLeafNumbers().sumNumbers(root);
// 输出结果
System.out.println("Sum of root-to-leaf numbers: " + sum);
}
}
19、判断二叉搜索树(Binary Search Tree,简称BST)
题目描述:给定一个二叉树,判断它是否是二叉搜索树(Binary Search Tree,简称BST)。
解题思路
判断一个树是否是二叉搜索树可以使用递归或迭代的方法来解决。二叉搜索树具有以下性质:
- 对于任意节点,其左子树的所有节点值都小于该节点的值。
- 对于任意节点,其右子树的所有节点值都大于该节点的值。
- 对于任意节点,其左子树和右子树都必须是二叉搜索树。
基于以上性质,我们可以采用以下方法判断一个树是否是二叉搜索树:
1. 递归法:
- 定义一个递归函数 isValidBST(root, minVal, maxVal),用于判断以 root 为根节点的子树是否是二叉搜索树。
- 如果 root 为空,返回 true。
- 如果 root 的值小于等于 minVal 或大于等于 maxVal,则说明不满足二叉搜索树的性质,返回 false。
- 递归调用 isValidBST 函数判断左子树和右子树是否是二叉搜索树,其中左子树的最大值应为 root 的值,右子树的最小值应为 root 的值。
- 如果左子树和右子树都是二叉搜索树,则说明以 root 为根节点的树也是二叉搜索树,返回 true。
2. 中序遍历法:
- 对二叉搜索树进行中序遍历,得到一个递增的节点值序列。
- 如果序列是递增的,则说明树是二叉搜索树;否则,树不是二叉搜索树。
通过上述方法,我们可以判断给定的二叉树是否是二叉搜索树。算法的时间复杂度为 O(n),其中 n 是树中节点的个数。
具体代码展示
package org.zyf.javabasic.letcode.tree;
import org.zyf.javabasic.letcode.tree.base.TreeNode;
/**
* @author yanfengzhang
* @description 给定一个二叉树,判断它是否是二叉搜索树(Binary Search Tree,简称BST)。
* @date 2023/6/12 23:02
*/
public class ValidateBST {
/**
* 判断一个树是否是二叉搜索树可以使用递归或迭代的方法来解决。二叉搜索树具有以下性质:
*
* 1. 对于任意节点,其左子树的所有节点值都小于该节点的值。
* 2. 对于任意节点,其右子树的所有节点值都大于该节点的值。
* 3. 对于任意节点,其左子树和右子树都必须是二叉搜索树。
*
* 基于以上性质,我们可以采用以下方法判断一个树是否是二叉搜索树:
*
* 1. 递归法:
* • 定义一个递归函数 isValidBST(root, minVal, maxVal),用于判断以 root 为根节点的子树是否是二叉搜索树。
* • 如果 root 为空,返回 true。
* • 如果 root 的值小于等于 minVal 或大于等于 maxVal,则说明不满足二叉搜索树的性质,返回 false。
* • 递归调用 isValidBST 函数判断左子树和右子树是否是二叉搜索树,其中左子树的最大值应为 root 的值,右子树的最小值应为 root 的值。
* • 如果左子树和右子树都是二叉搜索树,则说明以 root 为根节点的树也是二叉搜索树,返回 true。
* 2. 中序遍历法:
* • 对二叉搜索树进行中序遍历,得到一个递增的节点值序列。
* • 如果序列是递增的,则说明树是二叉搜索树;否则,树不是二叉搜索树。
*
* 通过上述方法,我们可以判断给定的二叉树是否是二叉搜索树。算法的时间复杂度为 O(n),其中 n 是树中节点的个数。
* @param root
* @return
*/
public boolean isValidBST(TreeNode root) {
return isValidBST(root, null, null);
}
private boolean isValidBST(TreeNode root, Integer minVal, Integer maxVal) {
if (root == null) {
return true;
}
if (minVal != null && root.val <= minVal) {
return false;
}
if(maxVal != null && root.val >= maxVal){
return false;
}
return isValidBST(root.left, minVal, root.val)
&& isValidBST(root.right, root.val, maxVal);
}
public static void main(String[] args) {
TreeNode root = new TreeNode(4);
root.left = new TreeNode(2);
root.right = new TreeNode(6);
root.left.left = new TreeNode(1);
root.left.right = new TreeNode(3);
root.right.left = new TreeNode(5);
root.right.right = new TreeNode(7);
ValidateBST solution = new ValidateBST();
boolean isValid = solution.isValidBST(root);
// 输出 Is Valid BST: true
System.out.println("Is Valid BST: " + isValid);
}
}