【LeetCode HOT 100】详细题解之二叉树篇
- 94 二叉树的中序遍历
- 方法一:递归
- 方法二:迭代
- 104 二叉树的最大深度
- 方法一:递归
- 方法二:迭代
- 226 翻转二叉树
- 方法一:递归
- 方法二:迭代
- 101 对称二叉树
- 方法一:递归
- 复杂度分析
- 注意事项
- 方法二:迭代
- 543 二叉树的直径
- 方法一:递归
- 复杂度分析
- 注意事项
- 102 二叉树的层序遍历
- 方法一:迭代
- 108 将有序数组转换为二叉搜索树
- 方法一:递归
- 98 验证二叉搜索树
- 方法一:递归
- 230 二叉搜索树中第k小的元素
- 方法一:递归
- 199 二叉树的右视图
- 方法一:层序遍历
- 114 二叉树展开为链表
- 方法一:递归
- 105 从前序与中序遍历序列构造二叉树
- 方法一:递归
- 问题分析
- 算法逻辑
- 437 路径总和III
- 方法一:递归+前缀和
- 问题分析
- 算法逻辑
- 236 二叉树的最近公共祖先
- 方法一:递归+回溯
- 124 二叉树中的最大路径和
- 方法一:递归
94 二叉树的中序遍历
94. 二叉树的中序遍历
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
提示:
- 树中节点数目在范围
[0, 100]
内 -100 <= Node.val <= 100
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
方法一:递归
注意,二叉树的中序遍历为左中右。因此递归时按该处理顺序处理即可。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
inorder(root,res);
return res;
}
private void inorder(TreeNode root,List<Integer> res){
if(root == null){
return;
}
inorder(root.left,res);
res.add(root.val);
inorder(root.right,res);
}
}
方法二:迭代
这里有点绕。定义一个访问节点来帮助迭代。
因为访问顺序和处理顺序不一致,访问是一路访问到最左子树才开始处理。因此在这里先定义一个访问的指针。
当指针不为空时,将访问的指针入栈,继续向左走,直到找到最左下面的节点。
找到后开始处理,先出栈,将出栈的节点的值放进res中,之后将指针修改为出栈节点的right,开始遍历右子树。
// 栈 先进后出
// 前序遍历,出栈顺序:根左右; 入栈顺序:右左根
// 中序遍历,出栈顺序:左根右; 入栈顺序:右根左
// 后序遍历,出栈顺序:左右根; 入栈顺序:根右左
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
//中序遍历这里需要注意的是,由于访问元素和处理的元素不一样
//所以写法不同于前序和后序,通过指针的遍历来访问结点,栈用来处理节点上的元素
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
TreeNode curr = root;//定义一个访问的指针
while(!stack.isEmpty() && curr != null){
//将访问的结点放进栈中
if(curr != null){
stack.push(curr);
curr = curr.left;
}else{
//出栈
curr = stack.pop();
res.add(curr.val);//中
curr = curr.right;//右
}
}
return res;
}
}
104 二叉树的最大深度
104. 二叉树的最大深度
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:3
示例 2:
输入:root = [1,null,2]
输出:2
提示:
- 树中节点的数量在
[0, 104]
区间内。 -100 <= Node.val <= 100
方法一:递归
递归三要素为
1.确定递归函数的参数和返回类型
2.递归终止条件
3.递归的单层处理逻辑
本题中需要求出二叉树的最大深度,因此递归参数应为TreeNode节点,返回类型为当前节点的最大深度
递归终止条件为:遍历到叶子节点时返回,因此if(root == null) return 0;
单层处理逻辑:当前节点的最大深度 = Math.max(左子树的最大深度,右子树的最大深度)+1,左子树和右子树的最大深度又可以以同样的方式进行计算。
因此,函数的主要部分为:
//根节点的深度为1,可以使用后序遍历,将左子树的深度和右子树的深度返回给根节点
//1.递归终止条件
if(root == null){
return 0;
}
//2.递归的单层处理逻辑
int LDepth = maxDepth(root.left);
int RDepth = maxDepth(root.right);
return Math.max(LDepth,RDepth) + 1;
最终代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
//根节点的深度为1,可以使用后序遍历,将左子树的深度和右子树的深度返回给根节点
if(root == null){
return 0;
}
int LDepth = maxDepth(root.left);
int RDepth = maxDepth(root.right);
return Math.max(LDepth,RDepth) + 1;
}
}
方法二:迭代
这里也可以用二叉树的层序遍历来解决。
层序遍历相较于递归来讲会更好理解。只需要统计遍历的最大层数,即为二叉树的最大深度.
在这里,将102 中层序遍历的解法稍微修改即可得到结果
代码如下
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
//方法二:层序遍历
if(root == null){
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;
while(!queue.isEmpty()){
depth ++; //深度+1
int currentSize = queue.size();
for(int i = 0;i<currentSize;i++){
TreeNode curr = queue.poll();
if(curr.left != null) queue.offer(curr.left);
if(curr.right != null) queue.offer(curr.right);
}
}
return depth;
}
}
226 翻转二叉树
226. 翻转二叉树
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
示例 2:
输入:root = [2,1,3]
输出:[2,3,1]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目范围在
[0, 100]
内 -100 <= Node.val <= 100
方法一:递归
本题解采用递归的方法来实现二叉树的反转。递归的基本思想是:对于树中的每个节点,先递归地交换其左右子树,然后再交换当前节点的左右子树。
-
递归终止条件:如果当前节点为空,则返回空。这是因为空节点不需要交换,且在树的遍历过程中,空节点表示已经到达了树的叶子节点的子节点。
-
单层处理逻辑:
(对于当前非空节点,首先递归地对左子树和右子树进行反转。这一步保证了在处理当前节点之前,其左右子树已经被反转。)翻转左子树:root.left = invertTree(root.left);
翻转右子树:root.right = invertTree(root.right);
交换左右子树:在递归调用返回后,当前节点的左右子树已经被反转。此时,通过交换当前节点的左右子树,完成当前节点的反转。
//交换当前节点的左右子树 TreeNode temp = root.left; root.left = root.right; root.right = temp;
-
返回:返回当前节点root
最终代码如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
//不停交换左右子树即可
//1.递归终止条件
if(root == null){
return null;
}
//2.单层处理逻辑
root.left = invertTree(root.left);
root.right = invertTree(root.right);
//交换当前节点的左右子树
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
return root;
}
}
复杂度分析:
时间复杂度 O(N) : 其中 N 为二叉树的节点数量,建立二叉树镜像需要遍历树的所有节点,占用 O(N) 时间。
空间复杂度 O(N) : 最差情况下(当二叉树退化为链表),递归时系统需使用 O(N) 大小的栈空间。
方法二:迭代
采用层序遍历的方法来实现二叉树的反转。层序遍历是一种广度优先遍历方法,它按照从上到下、从左到右的顺序遍历树的每一层。
- 初始化队列:使用一个队列来存储待处理的节点。
- 遍历队列:只要队列不为空,就继续处理队列中的节点。
- 处理当前层的节点:
- 记录当前层的节点数量。
- 遍历当前层的每个节点,对每个节点进行以下操作:
- 如果节点有左子节点,将其加入队列。
- 如果节点有右子节点,将其加入队列。
- 交换当前节点的左右子树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode invertTree(TreeNode root) {
//方法二:迭代
//层序遍历交换的思想是,将root的左右子树入队后,交换左右子树
Queue<TreeNode> queue = new LinkedList<TreeNode>();
if(root == null){
return root;
}
queue.offer(root);
while(!queue.isEmpty()){
int currentSize = queue.size();
for(int i = 0;i<currentSize;i++){
TreeNode curr = queue.poll();//从队列中获取队首元素,并让队首元素出队
if(curr.left!=null) queue.offer(curr.left);
if(curr.right!=null) queue.offer(curr.right);
//交换左右子树
TreeNode temp = curr.left;
curr.left = curr.right;
curr.right = temp;
}
}
return root;
}
}
101 对称二叉树
101. 对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
示例 1:
输入:root = [1,2,2,3,4,4,3]
输出:true
示例 2:
输入:root = [1,2,2,null,3,null,3]
输出:false
提示:
- 树中节点数目在范围
[1, 1000]
内 -100 <= Node.val <= 100
**进阶:**你可以运用递归和迭代两种方法解决这个问题吗?
方法一:递归
为了判断二叉树是否对称,需要递归地比较每个节点的左子树和右子树。对于每个节点,需要满足以下两个条件:
- 左子树的左子节点和右子树的右子节点相等。
- 左子树的右子节点和右子树的左子节点相等。
递归的基本思想是:
- 如果节点为空,返回
true
,因为空树是对称的。 - 如果左子节点不为空而右子节点为空,或者左子节点为空而右子节点不为空,返回
false
。 - 如果左右子节点都不为空,但值不相等,返回
false
。 - 如果左右子节点都不为空且值相等,递归地比较左子节点的左子树和右子树的右子树,以及左子节点的右子树和右子树的左子树。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return compare(root.left,root.right);
}
private boolean compare(TreeNode left,TreeNode right){
//方法一:递归
//直接判断左子树是否等于右子树并不可行。而是要判断
// 左子树的左子树是否等于右子树的右子树
// 左子树的右子树是否等于右子树的左子树
// 这样是因为,轴对称的话,要树的最左侧节点与最右侧节点相等(外侧)
// 树的内侧节点相等
// 比如在示例1中,我们要先判断最外侧 左边123 是否等于 右边123
// 之后再判断内侧 4 是否等于4
//1.递归终止条件
//左右子树不等,直接返回false
if(left != null && right == null){
return false;
}
if(left == null && right != null){
return false;
}
if(left == null && right == null){
return true;
}
if(left.val != right.val){
return false;
}
//2.开始判断
boolean outside = compare(left.left,right.right); //比较最外侧
boolean inside = compare(left.right,right.left); //比较最内侧
return inside && outside;
}
}
复杂度分析
- 时间复杂度:O(n),其中n为二叉树的节点数。每个节点恰好被访问一次。
- 空间复杂度:O(h),其中h为二叉树的高度。这是因为递归过程中最多会有h层的递归调用,每层递归调用会占用一定的栈空间。
注意事项
- 在比较节点值时,需要先检查节点是否为空,以避免空指针异常。
- 递归算法需要确保递归终止条件正确,否则会导致无限递归。
方法二:迭代
首先我们引入一个队列,这是把递归程序改写成迭代程序的常用方法。初始化时我们把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
//方法二:迭代
//此时的迭代并不是层序遍历,而是将我们要比较的元素成对的放入到队列中去进行比较
// 每次都取出队列中的前两个元素比较
// 1
// 2 2
//3 4 4 3
//第一次入队:push 1,1
// 进入while循环 取出队列中的前两个元素比较
// 之后将两个结点的左右子节点按相反的顺序插入队列中,即插入a的左节点,b的右节点,a的右节点,b的左节点,实现比较
Queue<TreeNode> queue = new LinkedList<TreeNode> ();
queue.offer(root);
queue.offer(root);
while(!queue.isEmpty()){
TreeNode root1 = queue.poll();
TreeNode root2 = queue.poll();
if( (root1 == null && root2 != null) || (root1 != null && root2 == null)){
return false;
}
if(root1 == null && root2 == null){
continue;
}
if(root1.val != root2.val){
return false;
}
queue.offer(root1.left); //比较外侧
queue.offer(root2.right);
queue.offer(root1.right); //比较内侧
queue.offer(root2.left);
}
return true;
}
}
543 二叉树的直径
543. 二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2]
输出:1
提示:
- 树中节点数目在范围
[1, 104]
内 -100 <= Node.val <= 100
方法一:递归
这个问题可以通过递归求解。对于每个节点,我们可以计算它的左子树和右子树的深度。那么,通过这个节点的最长路径(直径)就是左子树的深度加上右子树的深度。
- 递归终止条件:如果节点为空,返回0。空节点没有深度。
- 计算深度:对于每个非空节点,递归地计算其左子树和右子树的深度。
- 更新直径:更新全局变量
ans
,它保存了遍历过程中找到的最大直径。当前节点的直径是其左子树深度和右子树深度之和。 - 返回深度:返回节点的深度,这里我们返回的是子树中较大的深度加1,因为我们要计算的是以当前节点为根的子树的深度。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//存储遍历过程中找到的最大直径
int ans = Integer.MIN_VALUE;
public int diameterOfBinaryTree(TreeNode root) {
//递归求解,本题可以理解为求二叉树深度的变体。
//对每个节点求其左子树的深度和右子树的深度,那么该节点的直径即为左子树深度+右子树深度-1
//遍历每个节点,求出最大深度
depth(root);
return ans;
}
// 递归函数来计算节点的子树深度,并更新直径
public int depth(TreeNode root){
if(root == null){
return 0;
}
int LHeight = depth(root.left);
int RHeight = depth(root.right);
// 更新ans为左子树深度与右子树深度之和的最大值
// 这个和代表了通过当前节点的最长路径
ans = Math.max(ans,LHeight + RHeight);
// 返回子树中较大的深度加1,加1是因为也要计算当前节点的深度
// 这样在下次递归时可以计算通过当前节点的路径长度
return Math.max(LHeight,RHeight)+1;// 返回该层的深度
}
}
复杂度分析
- 时间复杂度:O(n),其中n为二叉树的节点数。每个节点恰好被访问一次。
- 空间复杂度:O(h),其中h为二叉树的高度。这是因为递归过程中最多会有h层的递归调用,每层递归调用会占用一定的栈空间。
注意事项
- 直径可能不经过根节点,因此我们不能简单地使用二叉树的最大深度来求解。
- 在更新直径时,需要考虑左右子树的深度之和。
- 递归算法需要确保递归终止条件正确,否则会导致无限递归。
102 二叉树的层序遍历
102. 二叉树的层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
示例 2:
输入:root = [1]
输出:[[1]]
示例 3:
输入:root = []
输出:[]
提示:
- 树中节点数目在范围
[0, 2000]
内 -1000 <= Node.val <= 1000
方法一:迭代
一开始想到了用队列,但是没有想到如何确认是哪一层的,后面发现可以先获取队列长度,通过这种方式来控制遍历的次数,从而取到某一层的结点。
其次就是注意根节点为空的特殊情况。
最后Queue队列的一些方法是poll和offer 一个出队一个入队
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
if(root == null){
return res;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); //添加元素
while(!queue.isEmpty()){
int currentLevelSize = queue.size();
List<Integer> level = new ArrayList<>();
//取出当前层的节点
for(int i = 0;i<currentLevelSize;i++){
TreeNode node = queue.poll(); //出队列
if(node != null){
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;
}
}
108 将有序数组转换为二叉搜索树
108. 将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵 平衡二叉搜索树。
示例 1:
输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:
示例 2:
输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums
按 严格递增 顺序排列
方法一:递归
为了将一个有序数组转换为一棵高度平衡的BST,可以采用递归的方法。基本思路是利用数组的有序性,在数组中间找到根节点的值,然后递归地将数组的左半部分构造为左子树,将右半部分构造为右子树。
- 递归终止条件:如果数组的起始索引大于或等于结束索引,则当前没有元素可以构造子树,返回
null
。 - 找到根节点:计算中间索引
mid
,将位于该索引的值作为当前子树的根节点。 - 构造左子树:递归地使用数组的左半部分(从起始索引到
mid
)来构造根节点的左子树。 - 构造右子树:递归地使用数组的右半部分(从
mid + 1
到结束索引)来构造根节点的右子树。 - 返回根节点:返回当前子树的根节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
// 这种构建二叉树的题目,关键是要确认如何才能递归的构建左子树和右子树
// 本题中,因为是有序的数组,所以我们只需要每次取出中间的元素作为根节点,左侧元素为左子树,右侧为右子树
// 通过这种方式递归的构建即可
return tranverse(nums,0,nums.length) ;//左闭右开
}
public TreeNode tranverse(int[] nums,int left,int right){
//递归终止
if(left >= right){
return null;
}
//计算中间索引
int mid = (left + right) /2;
//创建当前子树的根节点
TreeNode root = new TreeNode(nums[mid]);
//递归构建左子树
root.left = tranverse(nums,left,mid);
//递归构建右子树
root.right = tranverse(nums,mid+1,right);
//返回当前子树的根节点
return root;
}
}
复杂度分析
- 时间复杂度:O(n),其中n为数组的长度。每个元素恰好被访问一次。
- 空间复杂度:O(log n),这是递归过程中栈的深度,由于树是平衡的,所以递归深度是树的高度,对于平衡BST,高度大约是log(n)。
注意事项
- 确保递归的左右边界是正确的,左闭右开(
left
<=right
)。 - 计算中间索引时,使用
(left + right) / 2
可以避免在大数组中可能发生的整数溢出。
98 验证二叉搜索树
98. 验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
-
节点的左子树只包含 小于 当前节点的数。
-
节点的右子树只包含 大于 当前节点的数。
-
所有左子树和右子树自身必须也是二叉搜索树。
示例 1:
示例 2:
输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。
提示:
- 树中节点数目范围在
[1, 10^4]
内 -2^31 <= Node.val <= 2^31 - 1
方法一:递归
为了判断一个二叉树是否为BST,可以利用BST的性质:BST的中序遍历结果是一个递增的有序数组。因此,可以通过中序遍历二叉树,并检查遍历结果是否有序。
- 递归终止条件:如果当前节点为空,返回
true
。空树是BST。 - 中序遍历:递归地进行左子树的判断,然后检查当前节点,最后递归地进行右子树的判断。
- 当前节点的前一个节点:使用一个全局变量
pre
来记录中序遍历中的前一个访问的节点。 - 节点值的比较:在访问当前节点时,如果
pre
不为空且当前节点的值不大于pre
的值,则不是BST,返回false
。 - 更新前一个节点:将当前节点设置为
pre
,以便下一次递归时使用。 - 递归右子树:对右子树进行相同的判断。
- 组合左右子树的结果:当前节点的BST性质由左右子树的结果决定。
class Solution {
// 记录当前节点的前一个节点
TreeNode pre;
public boolean isValidBST(TreeNode root) {
// 二叉搜索树的性质为:中序遍历的结果一定是一个有序数组
// 我们可以利用这一性质,通过中序遍历,遍历时记录当前节点的前一个节点
// 判断前一个节点是否小于当前节点的值,若所有节点都满足返回true
// 1.递归终止条件
if (root == null) {
return true;
}
// 2.中序遍历
boolean left = isValidBST(root.left);
if (pre != null && pre.val >= root.val) {
return false; // 如果前一个节点的值大于或等于当前节点的值,则不是BST
}
pre = root; // 更新前一个节点为当前节点
boolean right = isValidBST(root.right);
return left && right; // 左右子树均满足BST性质时,当前树才满足BST性质
}
}
复杂度分析
- 时间复杂度:O(n),其中n为二叉树的节点数。每个节点恰好被访问一次。
- 空间复杂度:O(h),其中h为二叉树的高度。这是因为递归过程中最多会有h层的递归调用,每层递归调用会占用一定的栈空间。
注意事项
- 需要正确地维护前一个节点
pre
,它是中序遍历中最后一个访问的节点。 - 比较节点值时,需要确保
pre
不为空,因为根节点没有前一个节点。
230 二叉搜索树中第k小的元素
230. 二叉搜索树中第 K 小的元素
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
小的元素(从 1 开始计数)。
示例 1:
输入:root = [3,1,4,null,2], k = 1
输出:1
示例 2:
输入:root = [5,3,6,2,4,null,null,1], k = 3
输出:3
提示:
- 树中的节点数为
n
。 1 <= k <= n <= 104
0 <= Node.val <= 104
**进阶:**如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k
小的值,你将如何优化算法?
方法一:递归
这个问题可以通过中序遍历二叉树来解决。在中序遍历过程中,我们可以访问到节点的值,且值的顺序是递增的。因此,我们可以通过计数来找到第k个访问的节点。
- 初始化计数器:设置一个全局变量
tk
来记录还需要访问的节点数。 - 中序遍历:递归地遍历二叉树,先遍历左子树,然后访问当前节点,最后遍历右子树。
- 计数访问:每访问一个节点,
tk
减1。 - 记录第k小的元素:当
tk
减至0时,记录当前节点的值到ans
,并停止遍历。 - 返回结果:遍历结束后,返回
ans
。
class Solution {
int tk; // 记录还需要访问的节点数
int ans; // 存储第k小的元素
public int kthSmallest(TreeNode root, int k) {
tk = k; // 初始化计数器
tranverse(root); // 调用中序遍历方法
return ans; // 返回第k小的元素
}
public void tranverse(TreeNode root) {
if (root == null) {
return;
}
tranverse(root.left); // 遍历左子树
tk--; // 访问当前节点,计数器减1
if (tk == 0) { // 如果这是第k次访问
ans = root.val; // 更新答案为当前节点的值
return; // 找到后立即停止遍历
}
tranverse(root.right); // 遍历右子树
}
}
复杂度分析
- 时间复杂度:O(k + n),其中n为二叉树的节点数。在最坏情况下,可能需要访问所有节点才能找到第k小的元素。
- 空间复杂度:O(h),其中h为二叉树的高度。这是因为递归过程中最多会有h层的递归调用,每层递归调用会占用一定的栈空间。
199 二叉树的右视图
199. 二叉树的右视图
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
示例 2:
输入: [1,null,3]
输出: [1,3]
示例 3:
输入: []
输出: []
提示:
- 二叉树的节点个数的范围是
[0,100]
-100 <= Node.val <= 100
方法一:层序遍历
- 初始化:定义一个空列表
res
用来存储结果,如果根节点为空,直接返回空列表。 - 创建队列:使用
LinkedList
作为队列存储每层的节点。 - 根节点入队:将根节点添加到队列中。
- 遍历队列:当队列不为空时,执行以下步骤:
- 记录当前层的节点数量
currentSize
。 - 遍历当前层的每个节点,对于每个节点:
- 如果有左子节点,将其添加到队列中。
- 如果有右子节点,将其添加到队列中。
- 如果当前节点是该层的最后一个节点(即遍历的节点索引
i
等于currentSize - 1
),将其值添加到结果列表res
中。
- 记录当前层的节点数量
- 返回结果:遍历结束后,返回结果列表
res
。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
//题目直接用层序遍历即可求解
//层序遍历思路如下:
//1.根节点入队
//2.0 开始while循环,直到队列为空停止遍历
//2.记录该层节点数量n,(记录当前队列中的节点个数)
//3.遍历n次,每次遍历时将当前节点的左孩子和右孩子offer进队,同时poll掉当前节点
//从以上可以看出,本题可以在遍历每层的时候,将每层的最后一个元素记录到结果中,因为每层的最后一个元素一定是最右边的节点
//1.定义返回结果list
List<Integer> res = new ArrayList<>();
//根节点为空,返回空list
if(root == null){
return res;
}
//1.1 定义用来存储每层节点元素的队列,Deque是双端队列,我们在这里是需要实现先进先出即可。因此用Queue
Queue<TreeNode> queue = new LinkedList<>();
//2.根节点入队
queue.offer(root);
//2.0开始循环遍历,直到队列为空,说明遍历完毕跳出循环
while(!queue.isEmpty()){
//2.1 记录当前层的节点元素数量为currentSize
int currentSize = queue.size();
for(int i = 0;i<currentSize;i++){
TreeNode curr = queue.poll(); //获取队首的树节点,使用poll来移除并返回队首的元素
if(curr.left != null) queue.offer(curr.left); // 左子树不为空,入队
if(curr.right != null) queue.offer(curr.right); // 右子树不为空,入队
//如果是当前层最后一个元素,应当在结果中记录下来。
if(i == currentSize - 1){
res.add(curr.val);
}
}
}
return res;
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是二叉树的节点数。每个节点都被访问了一次。
- 空间复杂度:O(m),其中 m 是二叉树的宽度。在最坏的情况下,所有节点都在同一层,这时队列中存储的节点数最多,即二叉树的最大宽度。
114 二叉树展开为链表
114. 二叉树展开为链表
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
示例 1:
输入:root = [1,2,5,3,4,null,6]
输出:[1,null,2,null,3,null,4,null,5,null,6]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [0]
输出:[0]
提示:
- 树中结点数在范围
[0, 2000]
内 -100 <= Node.val <= 100
**进阶:**你可以使用原地算法(O(1)
额外空间)展开这棵树吗?
方法一:递归
本题和98题有点像。
通过递归实现。我们需要按照右左中的顺序遍历二叉树的节点,因为这样可以确保在处理当前节点之前,其左右子树已经被处理好并连接成了链表。
和先序遍历顺序相同,先序遍历为中左右。
考虑使用变形后序遍历的方式,即右左中来遍历,每次遍历都要记录当前节点遍历到的前一个节点为pre
之后更新当前节点的右子树为pre,左子树为null
为什么这样不用担心左子树丢失呢?因为按照右左中遍历顺序,一定是先将curr节点的右和左节点处理过后才处理curr
即curr的左子树已经连在链表中了因此可以直接左子树置为null
可以参考如下过程理解
我们知道题目给定的遍历顺序其实就是先序遍历的顺序,所以我们能不能利用先序遍历的代码,每遍历一个节点,就将上一个节点的右指针更新为当前节点。
先序遍历的顺序是 1 2 3 4 5 6。
遍历到 2,把 1 的右指针指向 2。1 -> 2 3 4 5 6。
遍历到 3,把 2 的右指针指向 3。1 -> 2 -> 3 4 5 6。
... ...
一直进行下去似乎就解决了这个问题。但现实是残酷的,原因就是我们把 1 的右指针指向 2,那么 1 的原本的右孩子就丢失了,也就是 5 就找不到了。
解决方法的话,我们可以逆过来进行。
我们依次遍历 6 5 4 3 2 1,然后每遍历一个节点就将当前节点的右指针更新为上一个节点。
遍历到 5,把 5 的右指针指向 6。6 <- 5 4 3 2 1。
遍历到 4,把 4 的右指针指向 5。6 <- 5 <- 4 3 2 1。
最终流程如下
- 递归终止条件:如果当前节点为
null
,则返回。 - 递归处理右子树:首先递归处理当前节点的右子树。
- 递归处理左子树:然后递归处理当前节点的左子树。
- 扁平化当前节点:将当前节点的左子树置为
null
,右子树指向前一个节点pre
。 - 更新前一个节点:将当前节点设置为前一个节点
pre
。 - 递归返回:递归返回,直到所有节点都被处理。
class Solution {
TreeNode pre = null;
public void flatten(TreeNode root) {
//1.确认递归终止条件
if(root == null){
return ;
}
//
if(root.right != null) flatten(root.right);
if(root.left != null) flatten(root.left);
//处理中间节点,将当前节点左子树执行null,右子树指向前一个节点
root.right = pre;
root.left = null;
pre = root;
return ;
}
}
105 从前序与中序遍历序列构造二叉树
105. 从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
示例 1:
输入: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
输出: [3,9,20,null,null,15,7]
示例 2:
输入: preorder = [-1], inorder = [-1]
输出: [-1]
提示:
1 <= preorder.length <= 3000
inorder.length == preorder.length
-3000 <= preorder[i], inorder[i] <= 3000
preorder
和inorder
均 无重复 元素inorder
均出现在preorder
preorder
保证 为二叉树的前序遍历序列inorder
保证 为二叉树的中序遍历序列
方法一:递归
问题分析
重建二叉树的问题可以通过递归的方法解决。关键在于理解前序遍历和中序遍历的特点:
- 前序遍历:首先访问根节点,然后递归访问左子树,最后递归访问右子树。
- 中序遍历:首先递归访问左子树,然后访问根节点,最后递归访问右子树。
根据以上特点,我们可以得出以下结论:
- 前序遍历的第一个节点总是当前子树的根节点。
- 在中序遍历中找到根节点,可以确定左子树和右子树的边界。
算法逻辑
- 构建映射:首先,我们需要构建一个映射(
HashMap
),将中序遍历中的每个值映射到其在数组中的索引。这样我们可以快速找到根节点在中序遍历中的位置。 - 递归终止条件:如果前序遍历的起始索引大于或等于结束索引,或者中序遍历的起始索引大于或等于结束索引,说明当前子树为空,返回
null
。 - 确定根节点:前序遍历的第一个节点是整棵树的根节点。
- 找到根节点在中序遍历中的位置:使用映射找到根节点在中序遍历中的索引。
- 计算左右子树的边界:
- 左子树的中序遍历序列从起始索引到根节点索引(不含)。
- 右子树的中序遍历序列从根节点索引的下一个位置到结束索引。
- 计算左子树的前序遍历序列:
- 左子树的前序遍历序列从根节点的下一个位置开始,长度等于左子树的中序遍历序列的长度。
- 递归构建左子树:使用左子树的前序和中序遍历序列递归构建左子树。
- 递归构建右子树:使用右子树的前序和中序遍历序列递归构建右子树。
- 组合子树:将递归构建的左右子树赋值给根节点的左右子节点。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//preorder = [3,9,20,15,7]
//inorder = [9,3,15,20,7]
//1.根据preorder找到根节点3
//2.根据根节点找到其在中序序列中的index = 1
//3.切割中序序列为左中序[,index),右中序[index+1,)
// 左中序[0,1),右中序[2,5)
//4.切割前序序列为左前序[preorderStart+1,preorderStart+1+左节点数量),右前序[preorderStart+1+左节点数量,)
// 左前序[1,2),右前序[2,5)
//5.递归构造左子树,右子树
Map<Integer,Integer> map = new HashMap<>();//保存中序序列的值的对应位置
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i = 0;i<inorder.length;i++){
map.put(inorder[i],i);
}
return buildTree(preorder,0,preorder.length,inorder,0,inorder.length);
}
private TreeNode buildTree(int[] preorder,int preorderStart,int preorderEnd,int[] inorder,int inorderStart,int inorderEnd){
if(preorderStart >= preorderEnd || inorderStart >= inorderEnd){
return null;
}
//1.根据preorder找到根节点3
int val = preorder[preorderStart];
TreeNode root = new TreeNode(val);
//2.根据根节点找到其在中序序列中的index = 1
int index = map.get(val);
//3.切割中序序列为左中序[,index),右中序[index+1,)
// 左中序[0,1),右中序[2,5)
int left_inorderStart = inorderStart;
int left_inorderEnd = index;
int right_inorderStart = index+1;
int right_inorderEnd = inorderEnd;
//4.切割前序序列为左前序[preorderStart+1,preorderStart+1+左节点数量),右前序[preorderStart+1+左节点数量,)
// 左前序[1,2),右前序[2,5)
int left_preorderStart = preorderStart+1;
int left_preorderEnd = preorderStart + 1 + left_inorderEnd - left_inorderStart;
int right_preorderStart = left_preorderEnd;
int right_preorderEnd = preorderEnd;
//5.递归构造左子树,右子树
root.left = buildTree(preorder,left_preorderStart,left_preorderEnd,inorder,left_inorderStart,left_inorderEnd);
root.right = buildTree(preorder,right_preorderStart,right_preorderEnd,inorder,right_inorderStart,right_inorderEnd);
return root;
}
}
437 路径总和III
437. 路径总和 III
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
提示:
- 二叉树的节点个数的范围是
[0,1000]
-109 <= Node.val <= 109
-1000 <= targetSum <= 1000
方法一:递归+前缀和
问题分析
这个问题可以通过递归和前缀和的思想来解决。前缀和是指从根节点开始,到当前节点为止的路径上的节点值之和。我们可以使用一个哈希表来存储前缀和的出现次数,这样当我们到达一个节点时,可以通过查找 (sum - targetSum)
的出现次数来确定有多少条路径的和等于 targetSum
。
算法逻辑
- 初始化:创建一个哈希表
cnt
,用于存储前缀和的出现次数。初始时,插入0
的计数为1
,因为如果路径和为0
,则没有节点,这是有效的。 - 递归终止条件:如果当前节点为
null
,则返回0
。 - 计算前缀和:到达当前节点时,计算从根节点到当前节点的路径和
sum
。 - 查找匹配的路径:使用
cnt.getOrDefault(sum - targetSum, 0)
查找有多少条路径的和为targetSum
。 - 更新哈希表:将当前的路径和
sum
更新到哈希表中。 - 递归遍历左右子树:对左右子树递归调用
tranverse
函数,并更新路径和。 - 回溯:在返回之前,将当前路径和
sum
的计数从哈希表中减去,以避免影响其他路径的计算。 - 返回结果:返回满足条件的路径数量。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//存放前缀和出现的次数
Map<Long,Integer> cnt = new HashMap<>();
public int pathSum(TreeNode root, int targetSum) {
//使用前缀和的思想来解决这道问题。
//二叉树中的前缀和定义为从根节点开始的路径元素和
//用哈希表cnt统计前缀和的出现次数,那么当递归到节点node的时候,假设从根到node的路径元素和为s,
//此时说明找到了cnt[s-targetSum]个符合要求的路径
//初始化时,哈希表中应先放入(0,1)
//遍历顺序可以用先序遍历从父节点到子节点
cnt.put(0L,1);
return tranverse(root,targetSum,0);
}
public int tranverse(TreeNode root,int targetSum,long sum){
//好抽象的题目,也可能是因为将二叉树和回溯+前缀和结合到一起。
//1.确定终止条件
if(root == null){
return 0;
}
//2.求出到当前节点的路径和(前缀和)
sum += root.val;
//3.这里,假如当前从根节点到当前节点和为10,targetSum为9,那么需要找的就是从根节点到某个节点路径和为1的路径数量
int ans = cnt.getOrDefault(sum-targetSum,0);
//3.1 更新哈希表
cnt.merge(sum,1,Integer::sum);
ans += tranverse(root.left,targetSum,sum);
ans += tranverse(root.right,targetSum,sum);
//3.2 回溯哈希表
cnt.merge(sum,-1,Integer::sum);
return ans;
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是二叉树的节点数。每个节点都被访问了一次。
- 空间复杂度:O(n),递归栈的深度是树的高度,最多为O(n)。哈希表的大小在最坏情况下也是O(n)。
注意事项
- 在递归过程中,确保在遍历完当前节点的左右子树后,回溯哈希表,以避免对后续计算造成影响。
- 使用
Long
类型来存储路径和,以避免可能的溢出。
236 二叉树的最近公共祖先
236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
示例 1:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。
示例 2:
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
示例 3:
输入:root = [1,2], p = 1, q = 2
输出:1
提示:
- 树中节点数目在范围
[2, 105]
内。 -109 <= Node.val <= 109
- 所有
Node.val
互不相同
。 p != q
p
和q
均存在于给定的二叉树中。
方法一:递归+回溯
问题分析
遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。那么二叉树如何可以自底向上查找呢?
通过回溯,二叉树回溯的过程就是从低到上。
后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。
接下来就看如何判断一个节点是节点q和节点p的公共祖先呢。
首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
这个问题可以通过递归和回溯的方法解决。我们可以使用后序遍历来找到 p
和 q
的最近公共祖先。
算法逻辑
- 递归终止条件:如果当前节点为空,返回
null
。 - 找到节点:如果当前节点是
p
或q
,返回当前节点。 - 递归遍历:递归地在左子树上调用
lowestCommonAncestor
函数,寻找p
和q
。 - 递归遍历:同样递归地在右子树上调用
lowestCommonAncestor
函数。 - 处理逻辑:
- 如果左子树和右子树的递归调用都返回了非
null
值,说明p
和q
分别在当前节点的左右子树中,当前节点就是它们的最近公共祖先。 - 如果左子树的递归调用返回了非
null
值而右子树返回了null
,说明p
或q
在左子树中,返回左子树的递归调用结果。 - 如果右子树的递归调用返回了非
null
值而左子树返回了null
,说明p
或q
在右子树中,返回右子树的递归调用结果。 - 如果左右子树的递归调用都返回了
null
,说明p
和q
都不在当前子树中,返回null
。
- 如果左子树和右子树的递归调用都返回了非
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//这里我们需要将节点的情况往根节点一步步返回。因此在这里需要使用后序遍历。左右中,将左右节点的情况返回给中间的根节点
//1.递归终止
if(root == null){
return null;
}
// 1.1 找到p或者q,将结果向上返回
if(root == p || root == q){
return root;
}
//2.开始递归
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
//3.判断左右节点中是否包含p或q,即判断left或者right是否为空
//3.1 如果left为空,right不为空,说明right出现了p或q,返回right
if(left == null && right != null){
return right;
}
//3.2 如果right为空,left不为空,说明left出现了p或q,返回left
if(left != null && right == null){
return left;
}
//3.3 如果right和left都不为空,返回root
if(left != null && right != null){
return root;
}
//3.4 否则返回null
return null;
}
}
复杂度分析
- 时间复杂度:O(n),其中 n 是二叉树的节点数。在最坏的情况下,每个节点都会被访问一次。
- 空间复杂度:O(h),其中 h 是二叉树的高度。这是因为在递归过程中,最多会有树的高度那么多层的函数调用在递归栈上。
注意事项
- 确保在递归过程中正确处理左右子树的返回值。
- 在二叉搜索树中,这个问题可以更高效地解决,因为可以通过比较节点值和查找值来决定是遍历左子树还是右子树。
124 二叉树中的最大路径和
124. 二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
提示:
- 树中节点数目范围是
[1, 3 * 104]
-1000 <= Node.val <= 1000
方法一:递归
问题分析
理解路径和的概念。在这里我们引入一个最大贡献值的概念。最大贡献值是指以当前节点为根的子树中,能够贡献给根节点的最大路径和。
整体思路为
对于根节点而言,其最大路径和=左子树的最大贡献值(左子树贡献值>0)+右子树的最大贡献值(右子树贡献值>0)
那最大贡献值如何计算呢?如果当前为叶子节点,那么最大贡献值就是他本身。如果不是叶子节点,则节点的最大贡献值= 当前节点的值 + 左/右子树节点的最大贡献值!
在这里我们需要将左子树和右子树的结果一层层向上返回,所以是后序遍历。左右中
算法逻辑
- 理解路径和:路径和是一条从节点到节点的路径上所有值的和,不能有重复的节点。
- 最大路径和的特点:最大路径和不一定经过根节点,因此我们需要在每个节点处计算以该节点为根的子树的最大路径和。
- 后序遍历:使用后序遍历(左右中)的方式遍历二叉树。这是因为我们需要先计算左右子树的最大贡献值,然后再计算当前节点的最大路径和。
- 递归终止条件:如果当前节点为空,返回0。这表示当前路径不存在。
- 计算最大贡献值:对于当前节点的左右子树,递归计算它们的最大贡献值。由于路径不能有重复节点,如果子树的最大贡献值小于0,则在计算当前节点的最大路径和时忽略该子树。
- 更新全局最大路径和:如果以当前节点为根的路径和大于全局最大路径和,则更新全局最大路径和。
- 返回最大贡献值:返回当前节点的最大贡献值,以便父节点计算其最大路径和。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
private int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
tranverse(root);
return maxSum;
}
public int tranverse(TreeNode root){
//记录每个节点的最大贡献值即可
//对于根节点而言,其最大路径和=左子树的最大贡献值(左子树贡献值>0)+右子树的最大贡献值(右子树贡献值>0)
//那最大贡献值如何计算呢?如果当前为叶子节点,那么最大贡献值就是他本身
//在这里我们需要将左子树和右子树的结果一层层向上返回,所以是后序遍历。左右中
if(root==null){
return 0;
}
//后序遍历,左右中
int LMax = Math.max(tranverse(root.left),0);
int RMax = Math.max(tranverse(root.right),0);
//中间节点
//节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = root.val + LMax + RMax;
maxSum = Math.max(priceNewpath,maxSum); //需要记录全局最大路径和。因为全局最大路径和不一定经过root节点
//节点的最大贡献值= 当前节点的值 + 左/右节点的最大贡献值!
return root.val + Math.max(LMax,RMax);
}
}