【LeetCode HOT 100】详细题解之二叉树篇

news2024/11/20 9:31:59

【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

方法一:递归

本题解采用递归的方法来实现二叉树的反转。递归的基本思想是:对于树中的每个节点,先递归地交换其左右子树,然后再交换当前节点的左右子树。

  1. 递归终止条件:如果当前节点为空,则返回空。这是因为空节点不需要交换,且在树的遍历过程中,空节点表示已经到达了树的叶子节点的子节点。

  2. 单层处理逻辑
    (对于当前非空节点,首先递归地对左子树和右子树进行反转。这一步保证了在处理当前节点之前,其左右子树已经被反转。)

    翻转左子树:root.left = invertTree(root.left);

    翻转右子树:root.right = invertTree(root.right);

    交换左右子树:在递归调用返回后,当前节点的左右子树已经被反转。此时,通过交换当前节点的左右子树,完成当前节点的反转。

        //交换当前节点的左右子树
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
    
  3. 返回:返回当前节点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) 大小的栈空间。

方法二:迭代

采用层序遍历的方法来实现二叉树的反转。层序遍历是一种广度优先遍历方法,它按照从上到下、从左到右的顺序遍历树的每一层。

  1. 初始化队列:使用一个队列来存储待处理的节点。
  2. 遍历队列:只要队列不为空,就继续处理队列中的节点。
  3. 处理当前层的节点
    • 记录当前层的节点数量。
    • 遍历当前层的每个节点,对每个节点进行以下操作:
      • 如果节点有左子节点,将其加入队列。
      • 如果节点有右子节点,将其加入队列。
      • 交换当前节点的左右子树。
/**
 * 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:

img

输入:root = [1,2,2,3,4,4,3]
输出:true

示例 2:

img

输入:root = [1,2,2,null,3,null,3]
输出:false

提示:

  • 树中节点数目在范围 [1, 1000]
  • -100 <= Node.val <= 100

**进阶:**你可以运用递归和迭代两种方法解决这个问题吗?

方法一:递归

为了判断二叉树是否对称,需要递归地比较每个节点的左子树和右子树。对于每个节点,需要满足以下两个条件:

  1. 左子树的左子节点和右子树的右子节点相等。
  2. 左子树的右子节点和右子树的左子节点相等。

递归的基本思想是:

  • 如果节点为空,返回 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

方法一:递归

这个问题可以通过递归求解。对于每个节点,我们可以计算它的左子树和右子树的深度。那么,通过这个节点的最长路径(直径)就是左子树的深度加上右子树的深度。

  1. 递归终止条件:如果节点为空,返回0。空节点没有深度。
  2. 计算深度:对于每个非空节点,递归地计算其左子树和右子树的深度。
  3. 更新直径:更新全局变量 ans,它保存了遍历过程中找到的最大直径。当前节点的直径是其左子树深度和右子树深度之和。
  4. 返回深度:返回节点的深度,这里我们返回的是子树中较大的深度加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,可以采用递归的方法。基本思路是利用数组的有序性,在数组中间找到根节点的值,然后递归地将数组的左半部分构造为左子树,将右半部分构造为右子树。

  1. 递归终止条件:如果数组的起始索引大于或等于结束索引,则当前没有元素可以构造子树,返回 null
  2. 找到根节点:计算中间索引 mid,将位于该索引的值作为当前子树的根节点。
  3. 构造左子树:递归地使用数组的左半部分(从起始索引到mid)来构造根节点的左子树。
  4. 构造右子树:递归地使用数组的右半部分(从mid + 1到结束索引)来构造根节点的右子树。
  5. 返回根节点:返回当前子树的根节点。
/**
 * 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的中序遍历结果是一个递增的有序数组。因此,可以通过中序遍历二叉树,并检查遍历结果是否有序。

  1. 递归终止条件:如果当前节点为空,返回 true。空树是BST。
  2. 中序遍历:递归地进行左子树的判断,然后检查当前节点,最后递归地进行右子树的判断。
  3. 当前节点的前一个节点:使用一个全局变量 pre 来记录中序遍历中的前一个访问的节点。
  4. 节点值的比较:在访问当前节点时,如果 pre 不为空且当前节点的值不大于 pre 的值,则不是BST,返回 false
  5. 更新前一个节点:将当前节点设置为 pre,以便下一次递归时使用。
  6. 递归右子树:对右子树进行相同的判断。
  7. 组合左右子树的结果:当前节点的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个访问的节点。

  1. 初始化计数器:设置一个全局变量 tk 来记录还需要访问的节点数。
  2. 中序遍历:递归地遍历二叉树,先遍历左子树,然后访问当前节点,最后遍历右子树。
  3. 计数访问:每访问一个节点,tk 减1。
  4. 记录第k小的元素:当 tk 减至0时,记录当前节点的值到 ans,并停止遍历。
  5. 返回结果:遍历结束后,返回 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

方法一:层序遍历

  1. 初始化:定义一个空列表 res 用来存储结果,如果根节点为空,直接返回空列表。
  2. 创建队列:使用 LinkedList 作为队列存储每层的节点。
  3. 根节点入队:将根节点添加到队列中。
  4. 遍历队列:当队列不为空时,执行以下步骤:
    • 记录当前层的节点数量 currentSize
    • 遍历当前层的每个节点,对于每个节点:
      • 如果有左子节点,将其添加到队列中。
      • 如果有右子节点,将其添加到队列中。
      • 如果当前节点是该层的最后一个节点(即遍历的节点索引 i 等于 currentSize - 1),将其值添加到结果列表 res 中。
  5. 返回结果:遍历结束后,返回结果列表 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。

最终流程如下

  1. 递归终止条件:如果当前节点为 null,则返回。
  2. 递归处理右子树:首先递归处理当前节点的右子树。
  3. 递归处理左子树:然后递归处理当前节点的左子树。
  4. 扁平化当前节点:将当前节点的左子树置为 null,右子树指向前一个节点 pre
  5. 更新前一个节点:将当前节点设置为前一个节点 pre
  6. 递归返回:递归返回,直到所有节点都被处理。
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. 从前序与中序遍历序列构造二叉树

给定两个整数数组 preorderinorder ,其中 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
  • preorderinorder无重复 元素
  • inorder 均出现在 preorder
  • preorder 保证 为二叉树的前序遍历序列
  • inorder 保证 为二叉树的中序遍历序列

方法一:递归

问题分析

重建二叉树的问题可以通过递归的方法解决。关键在于理解前序遍历和中序遍历的特点:

  1. 前序遍历:首先访问根节点,然后递归访问左子树,最后递归访问右子树。
  2. 中序遍历:首先递归访问左子树,然后访问根节点,最后递归访问右子树。

根据以上特点,我们可以得出以下结论:

  • 前序遍历的第一个节点总是当前子树的根节点。
  • 在中序遍历中找到根节点,可以确定左子树和右子树的边界。
算法逻辑
  1. 构建映射:首先,我们需要构建一个映射(HashMap),将中序遍历中的每个值映射到其在数组中的索引。这样我们可以快速找到根节点在中序遍历中的位置。
  2. 递归终止条件:如果前序遍历的起始索引大于或等于结束索引,或者中序遍历的起始索引大于或等于结束索引,说明当前子树为空,返回 null
  3. 确定根节点:前序遍历的第一个节点是整棵树的根节点。
  4. 找到根节点在中序遍历中的位置:使用映射找到根节点在中序遍历中的索引。
  5. 计算左右子树的边界
    • 左子树的中序遍历序列从起始索引到根节点索引(不含)。
    • 右子树的中序遍历序列从根节点索引的下一个位置到结束索引。
  6. 计算左子树的前序遍历序列
    • 左子树的前序遍历序列从根节点的下一个位置开始,长度等于左子树的中序遍历序列的长度。
  7. 递归构建左子树:使用左子树的前序和中序遍历序列递归构建左子树。
  8. 递归构建右子树:使用右子树的前序和中序遍历序列递归构建右子树。
  9. 组合子树:将递归构建的左右子树赋值给根节点的左右子节点。
/**
 * 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

算法逻辑
  1. 初始化:创建一个哈希表 cnt,用于存储前缀和的出现次数。初始时,插入 0 的计数为 1,因为如果路径和为 0,则没有节点,这是有效的。
  2. 递归终止条件:如果当前节点为 null,则返回 0
  3. 计算前缀和:到达当前节点时,计算从根节点到当前节点的路径和 sum
  4. 查找匹配的路径:使用 cnt.getOrDefault(sum - targetSum, 0) 查找有多少条路径的和为 targetSum
  5. 更新哈希表:将当前的路径和 sum 更新到哈希表中。
  6. 递归遍历左右子树:对左右子树递归调用 tranverse 函数,并更新路径和。
  7. 回溯:在返回之前,将当前路径和 sum 的计数从哈希表中减去,以避免影响其他路径的计算。
  8. 返回结果:返回满足条件的路径数量。
/**
 * 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
  • pq 均存在于给定的二叉树中。

方法一:递归+回溯

问题分析

遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。那么二叉树如何可以自底向上查找呢?

通过回溯,二叉树回溯的过程就是从低到上。

后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。

接下来就看如何判断一个节点是节点q和节点p的公共祖先呢。

首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。

这个问题可以通过递归和回溯的方法解决。我们可以使用后序遍历来找到 pq 的最近公共祖先。

算法逻辑

  1. 递归终止条件:如果当前节点为空,返回 null
  2. 找到节点:如果当前节点是 pq,返回当前节点。
  3. 递归遍历:递归地在左子树上调用 lowestCommonAncestor 函数,寻找 pq
  4. 递归遍历:同样递归地在右子树上调用 lowestCommonAncestor 函数。
  5. 处理逻辑
    • 如果左子树和右子树的递归调用都返回了非 null 值,说明 pq 分别在当前节点的左右子树中,当前节点就是它们的最近公共祖先。
    • 如果左子树的递归调用返回了非 null 值而右子树返回了 null,说明 pq 在左子树中,返回左子树的递归调用结果。
    • 如果右子树的递归调用返回了非 null 值而左子树返回了 null,说明 pq 在右子树中,返回右子树的递归调用结果。
    • 如果左右子树的递归调用都返回了 null,说明 pq 都不在当前子树中,返回 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)
那最大贡献值如何计算呢?如果当前为叶子节点,那么最大贡献值就是他本身。如果不是叶子节点,则节点的最大贡献值= 当前节点的值 + 左/右子树节点的最大贡献值!
在这里我们需要将左子树和右子树的结果一层层向上返回,所以是后序遍历。左右中

算法逻辑

  1. 理解路径和:路径和是一条从节点到节点的路径上所有值的和,不能有重复的节点。
  2. 最大路径和的特点:最大路径和不一定经过根节点,因此我们需要在每个节点处计算以该节点为根的子树的最大路径和。
  3. 后序遍历:使用后序遍历(左右中)的方式遍历二叉树。这是因为我们需要先计算左右子树的最大贡献值,然后再计算当前节点的最大路径和。
  4. 递归终止条件:如果当前节点为空,返回0。这表示当前路径不存在。
  5. 计算最大贡献值:对于当前节点的左右子树,递归计算它们的最大贡献值。由于路径不能有重复节点,如果子树的最大贡献值小于0,则在计算当前节点的最大路径和时忽略该子树。
  6. 更新全局最大路径和:如果以当前节点为根的路径和大于全局最大路径和,则更新全局最大路径和。
  7. 返回最大贡献值:返回当前节点的最大贡献值,以便父节点计算其最大路径和。
/**
 * 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);
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

麦克斯韦方程组的独立方程与辅助方程

第四个方程可以由第一个方程得到。注意哈密顿算符求散度是对空间求偏导&#xff0c;也就是xyz&#xff0c;而麦克斯韦方程组中的矢量除了xyz还有t也是自变量。 方程1推出方程4&#xff08;用到求微分的换顺序&#xff09; 方程2推出方程3&#xff0c;用到电流面密度和电荷体密…

python调用opencv报错“module ‘cv2‘ has no attribute ‘namedWindow‘”

之前电脑上使用pip install安装过opencv相关的python模块&#xff0c;不过后续学习opencv时主要使用OpenCVSharp在VS2022中创建项目测试。今天学习过程中突然想用python试试&#xff0c;不过运行下面代码时报错“module ‘cv2’ has no attribute namedWindow”。 import cv2c…

单调队列与单调栈<2>——单调栈

单调栈的定义 单调递增栈 栈中元素从栈底到栈顶是递增的。 单调递减栈 栈中元素从栈底到栈顶是递减的。 单调栈的核心内容 我们从左到右遍历元素&#xff0c;构造单调栈&#xff08;从栈顶到栈底递增或减&#xff09;&#xff1a;在 i 从左往右遍历的过程中&#xff0c;我…

【高频SQL基础50题】16-20

day by day. 目录 1.进店却未进行过交易的顾客 2.项目员工 I 3.销售分析III 4. 判断三角形 5. 电影评分 1.进店却未进行过交易的顾客 连接题。 思路&#xff1a;根据trans表中的visit_id号在 visits表中排除&#xff0c;再将剩下的合并相同客户&#xff08;累加visit…

300Kg载重履带式无人车底盘定制开发技术详解

关于300Kg载重履带式无人车底盘的定制开发技术&#xff0c;可以从以下几个方面进行详细解析&#xff1a; 一、总体设计思路 300Kg载重履带式无人车底盘的定制开发&#xff0c;旨在打造一款承载能力强、行驶稳定、适应复杂地形的无人运输平台。设计过程中需充分考虑动力系统的…

【C++ 前缀和 数论】1590. 使数组和能被 P 整除|2038

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 质数、最大公约数、菲蜀定理 LeetCode 1590. 使数组和能被 P 整除 给你一个正整数数组 nums&#xff0c;请你移除 最短 子数组&#xff08;可以为 空&#xff09;&am…

麒麟系统文件扩展名所表示的文件种类

图 1 .conf 配置文件&#xff0c;如图2&#xff1b; 图 2 .Deb 安装包&#xff0c;如图3&#xff1b; 图 3 .sh 可执行程序&#xff0c;如图4&#xff1b; 图 4 .tar或者.gz 压缩包&#xff0c;如图5&#xff1b; 图 5 .bin 二进制文件&#xff0c;如图6&#xff1b; 图 6…

android compose ScrollableTabRow indicator 指示器设置宽度

.requiredWidth(30.dp) Box(modifier Modifier.background(Color.LightGray).fillMaxWidth()) {ScrollableTabRow(selectedTabIndex selectedTabIndex, // 默认选中第一个标签containerColor ColorPageBg,edgePadding 1.dp, // 内容与边缘的距离indicator { tabPositions…

房地产政策持续利好,龙湖“好房子”新品观萃入市受市场认可

《港湾商业观察》王璐 经过疫情以来持续调整的房地产市场&#xff0c;外界预期&#xff0c;在政策利好效应带动下&#xff0c;行业将稳健向前。 9月26日&#xff0c;中央政治局会议对房地产的表述内容释放出极强的政策宽松信号。会议首次明确提出&#xff0c;“要促进房地产市…

解决QWidget及内部控件只能放大不能缩小的问题

在设计可缩放控件的时候&#xff0c;你可能会与到这种情况&#xff1a; 缩放 控件只能放大&#xff0c;不能缩小。。。是不是很头痛&#xff1f;让我来猜猜原因&#xff1a;你是不是让子类控件的大小设置地跟父类控件大小一样大&#xff1f;没错这就是原因所在。 用上面一行替换…

pdf怎么盖章不覆盖文字

pdf怎么盖章不覆盖文字&#xff1f;在PDF上盖章而不覆盖文字&#xff0c;可以按照以下步骤操作&#xff1a; 使用福昕PDF编辑器 下载安装&#xff1a;从福昕软件官网下载并安装福昕PDF编辑器。打开文档&#xff1a;用福昕PDF编辑器打开需要盖章的PDF文档。访问图章工具&#…

[网络]抓包工具介绍 tcpdump

一、tcpdump tcpdump是一款基于命令行的网络抓包工具&#xff0c;可以捕获并分析传输到和从网络接口流入和流出的数据包。 1.1 安装 tcpdump 通常已经预装在大多数 Linux 发行版中。如果没有安装&#xff0c;可以使用包管理器 进行安装。例如 Ubuntu&#xff0c;可以使用以下…

Ps:将画板导出到 PDF

菜单&#xff1a;文件/导出/将画板导出到 PDF Export/Artboards to PDF 将画板导出到 PDF Artboards to PDF命令用于将 Photoshop 的画板导出为 PDF 文件&#xff0c;提供了多种导出选项&#xff0c;可以控制文件的压缩、是否嵌入颜色配置文件、文件命名以及页面顺序等。它适用…

电力、电子、电气、电器,怎么说明白?

从学科划分的角度讲 电力、电子和电器都属于电气工程学科。电气工程一词来源于日本&#xff0c;其中的“气”来源于西方外文翻译&#xff08;西方工业之初&#xff0c;动力机械均由蒸汽轮机驱动,后来用与发电&#xff0c;故"电气"开始乏指工业动力&#xff0c;现在电…

高效便捷,体验不一样的韩语翻译神器

嘿&#xff0c;大家好啊&#xff01;今天想跟大家聊聊我用过的几款翻译神器&#xff0c;特别是它们在翻译韩语时的那些小感受。作为一个偶尔需要啃啃韩语资料或者跟韩国朋友聊天的普通人&#xff0c;我真心觉得这些翻译工具简直就是我的救星&#xff01; 一、福昕在线翻译 网址…

Unity网络开发记录(五):二进制传输消息类

消息类 前篇所写的CS模型只能传输字符串&#xff0c;在本篇文章中&#xff0c;尝试在服务端一次传输多种类型的数据&#xff0c;并在客户端接收到信息之后进行解析显示 为了实现对不同类型的数据传输&#xff0c;需要将不同类型的数据都转换成二进制的形式在网络中进行传输。所…

爬虫——爬虫理论+request模块

一、爬虫理论 爬虫——请求网站并提取数据的自动化程序 网络爬虫&#xff08;又被称为网页蜘蛛&#xff0c;网络机器人&#xff09;就是模拟客户端发送网络请求&#xff0c;接收请求响应&#xff0c;一种按照一定的规则&#xff0c;自动的抓取互联网信息的程序。 原则上&…

通信工程学习:什么是HTTP超文本传输协议

HTTP&#xff1a;超文本传输协议 HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;&#xff0c;即超文本传输协议&#xff0c;是互联网上应用最为广泛的一种网络协议&#xff0c;它位于TCP/IP四层模型当中的应用层。以下是关于HTTP协议的详细解释&#xff1a; 一、H…

太速科技-495-定制化仪器户外便携式手提触摸一体机

定制化仪器户外便携式手提触摸一体机 一、产品简介 数据处理便携式机箱平台以mini-ITX主板为基础&#xff0c;集成intel CPU处理器、FPGA、ADDA、光纤等接口&#xff0c;集成触摸显示屏&#xff0c;抗震机箱。设备小型化、一体化&#xff0c;适合实验室、办公桌面、野外…

Cannon-es.js之removeConstraint破坏约束案例

本文目录 前言最终效果1、postStep2、前置准备2.1 代码2.2 效果 3、removeConstraint3.1 解除约束代码效果 4、完整代码 前言 在3D物理引擎的广阔天地中&#xff0c;cannon-es以其轻量级、高性能和易于集成的特点&#xff0c;成为了WebGL环境中物理模拟的首选工具。它不仅能够精…