Binary Tree

259 阅读22分钟

1. 二叉树的基本理论

1.1 二叉树的种类

1.1.1 满二叉树 Full Binary Tree

深度为k,有2^k-1个节点的二叉树

满二叉树

1.1.2 完全二叉树 Complete Binary Tree

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1)  个节点。

完全二叉树判断

堆就是一颗完全二叉树

1.1.3 二叉搜索树 Binary Search Tree (BST)

二叉搜索树是一个有序树

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树

二叉搜索树

1.1.4 平衡二叉搜索树 AVL

AVL: (Adelson-Velsky and Landis):它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

AVL

最后一棵不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。

1.2 二叉树的储存方式

1.2.1 链式储存

用指针,通过指针把分布在散落在各个地址的节点串联一起。

链式储存

1.2.2 顺序储存

用数组,存储的元素在内存是连续分布的

顺序储存 如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。

1.3 二叉树的遍历

  1. 深度优先DFS -- Stack
  • 前序遍历:根左右
  • 中序遍历:左根右
  • 后序遍历:左右根

DFS遍历 2. 广度优先BFS -- Queue

  • 层序遍历

1.4 二叉树的定义

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;
    }
}

2. 二叉树的遍历

2.1 二叉树的递归遍历 DFS

递归三要素:

  1. 确定递归函数的参数和返回值:  确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件:  写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑:  确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

2.1.1 前序 144. Binary Tree Preorder Traversal 💚

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        preOrder(root,res);
        return res;
    }
    
    public void preOrder(TreeNode root, List<Integer> res){
        if(root == null){
            return;
        }
        res.add(root.val);
        preOrder(root.left, res);
        preOrder(root.right, res);
    }
}

2.1.2 中序 94. Binary Tree Inorder Traversal 💚

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        inOrder(root, res);
        return res;
    }
    public void inOrder(TreeNode root, List<Integer> res){
        if(root == null){
            return;
        }
        inOrder(root.left, res);
        res.add(root.val);
        inOrder(root.right, res);
    }
}

2.1.3 后序 145. Binary Tree Postorder Traversal 💚

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        postOrder(root, res);
        return res;
    }
    
    public void postOrder(TreeNode root, List<Integer> res){
        if(root == null){
            return;
        }
        postOrder(root.left, res);
        postOrder(root.right, res);
        res.add(root.val);
    }
}

2.2 二叉树的非递归遍历 Stack

递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。

所以用栈也可以是实现二叉树的前后中序遍历。

2.2.1 前序

前序遍历是根左右,每次先处理的是中间节点,先将根节点放入栈中,然后弹出并打印,然后将右孩子加入栈,再加入左孩子。这样出栈的顺序就是根左右。 前序遍历

//前序遍历顺序:中-左-右,入栈顺序:中-右-左

class Solution{
    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 temp = stack.pop();
            res.add(temp.val);
            if(temp.right != null){
                stack.push(temp.right);
            }
            if(temp.left != null){
                stack.push(temp.left);
            }
        }
        return res;
    }
}

2.1.2 中序

目前的前序遍历的逻辑无法直接应用到中序遍历上。

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

中序遍历

具体思路:

  • 整个树的左边界依次入栈
  • 依次弹出,弹出时打印
  • 若弹出节点有右树,则重复以上步骤(右树的整个左边界依次入栈)
  • 打印整个栈
//中序遍历顺序:左根右 入栈顺序左右
class Solution{
    public List<Integer> inorderTraversal(TreeNode root){
        List<Integer> res = new ArrayList<>();
        if(root == null){
            return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        TreeNode temp = root;
        while(temp != null || !stack.isEmpty()){
            if(temp != null){
                stack.push(temp);
                temp = temp.left;//把左边界全部入栈
            }else{//temp为空,说明左边界已经全部入栈
                temp = stack.pop();//弹出并打印
                res.add(temp.val);
                temp = temp.right; //节点变成右孩子,此时temp如果有左孩子,就非空,继续把左边界全部压入栈 
            }
        }
        return res;
    }
}

2.1.3 后序

后序遍历就是左右根,先序遍历是根左右,后续遍历是左右根,那么我们只需要调整一下先序遍历的代码顺序,就变成根右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了

后序遍历

//后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution{
    public List<Integer> postorderTraversal(TreeNode root){
        List<Integer> res = new ArrayList<>();
        if(root == null){
            return res;
        }
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()){
            TreeNode temp = stack.pop();
            res.add(temp.val);
            if(temp.left != null){
                stack.push(temp.left);
            }
            if(temp.right != null){
                stack.push(temp.right);
            }
        }
        Collections.reverse(res);
        return res;
    }
}

2.3 二叉树层序遍历 BFS

BFS遍历二叉树的套路非常固定~ 需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。

二叉树的层序遍历BFS

很重要的一点就是需要用一个变量记录每层节点的个数,这样才能知道那些点属于同一层

2.3.1 二叉树的层序遍历 102. Binary Tree Level Order Traversal 🧡

class Solution{
    public List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> levelOrder(TreeNode root){
        process(root);
        return res;
    }
    
    public void process(TreeNode root){
        if(root == null){
            return;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            List<Integer> oneres = new ArrayList<>();
            int len = queue.size();
            while(len > 0){
                TreeNode temp = queue.poll();
                oneres.add(temp.val);
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
                len--;
            }
            res.add(oneres);
        }
    }
}

2.3.2 二叉树的层次遍历II 107. Binary Tree Level Order Traversal II 🧡

相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。

class Solution {
    public List<List<Integer>> levelOrderBottom(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        
        if(root == null){
            return res;//记得判空!
        }
        
        while(!queue.isEmpty()){
            List<Integer> oneres = new ArrayList<>();
            int len = queue.size();
            while(len > 0){
                TreeNode temp = queue.poll();
                oneres.add(temp.val);
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
                len--;
            }
            res.add(oneres);
        }
        List<List<Integer>> finalres = new ArrayList<>();
        for(int i = res.size() - 1 ; i >= 0; i--){
            finalres.add(res.get(i));
        }
        return finalres;
    }
}

2.3.3 二叉树的右视图 199. Binary Tree Right Side View 🧡

层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。

class Solution {
    public List<Integer> rightSideView(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            int len = queue.size();
            for(int i = 0; i < len; i++){
                TreeNode temp = queue.poll();
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
                if(i == len - 1){
                    res.add(temp.val);
                }
            }
        }
        return res;
    }
}

2.3.4 .二叉树的层平均值 637. Average of Levels in Binary Tree 💚

本题就是层序遍历的时候把一层求个总和在取一个均值。

class Solution {
    public List<Double> averageOfLevels(TreeNode root) {
        List<Double> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            double len = queue.size();
            double sum = 0;
            for(int i = 0; i < len; i++){
                TreeNode temp = queue.poll();
                sum += temp.val;
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
            }
            double oneres = sum/len;
            res.add(oneres);
        }
        return res;
    }
}

2.3.5 N叉树的层序遍历 429. N-ary Tree Level Order Traversal🧡

这道题依旧是模板题,只不过一个节点有多个孩子了。要记住多个孩子遍历的写法

class Solution {
    public List<List<Integer>> levelOrder(Node root) {
        List<List<Integer>> res = new ArrayList<>();
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        
        if(root == null){
            return res;
        }
        while(!queue.isEmpty()){
            List<Integer> oneres = new ArrayList<>();
            int len = queue.size();
            for(int i = 0; i < len; i++){
                Node temp = queue.poll();
                oneres.add(temp.val);
                List<Node> children = temp.children;
                if(children == null || children.size() == 0){
                    continue;
                }
                for(Node child:children){
                    queue.offer(child);
                }
            }
            res.add(oneres);
        }
        return res;
    }
}

2.3.6 在每个树行中找最大值 515. Find Largest Value in Each Tree Row 🧡

层序遍历,取每一层的最大值

class Solution {
    public List<Integer> largestValues(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        if(root == null){
            return res;
        }
        while(!queue.isEmpty()){
            int len = queue.size();
            int max = Integer.MIN_VALUE;
            for(int i = 0; i < len; i++){
                TreeNode temp = queue.poll();
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
                int tempval = temp.val;
                if(tempval > max){
                    max = tempval;
                }
            }
            res.add(max);
        }
        return res;
    }
}

2.3.7 填充每个节点的下一个右侧节点指针 116. Populating Next Right Pointers in Each Node 🧡

本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了。需要特殊处理每层的第一个节点

class Solution {
    public Node connect(Node root) {
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        if(root == null){
            return root;
        }
        while(!queue.isEmpty()){
            int len = queue.size();
            Node cur = queue.poll();
            if(cur.left != null){
                queue.offer(cur.left);
            }
            if(cur.right != null){
                queue.offer(cur.right);
            }
            for(int i = 1; i < len; i++){
                Node next = queue.poll();
                if(next.left != null){
                    queue.offer(next.left);
                }
                if(next.right != null){
                    queue.offer(next.right);
                }   
                cur.next = next;
                cur = next;
            }
        }
        return root;
    }
}

2.3.8 填充每个节点的下一个右侧节点指针II 117. Populating Next Right Pointers in Each Node II 🧡

这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道。还是注意特殊处理每层的第一个节点。

class Solution {
    public Node connect(Node root) {
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        if(root == null){
            return root;
        }
        while(!queue.isEmpty()){
            int len = queue.size();
            Node cur = queue.poll();
            if(cur.left != null){
                queue.offer(cur.left);
            }
            if(cur.right != null){
                queue.offer(cur.right);
            }
            for(int i = 1; i < len; i++){
                Node next = queue.poll();
                if(next.left != null){
                    queue.offer(next.left);
                }
                if(next.right != null){
                    queue.offer(next.right);
                }   
                cur.next = next;
                cur = next;
            }
        }
        return root;
    }
}

3. 二叉树的属性

3.1 对称二叉树

3.1.1 对称二叉树 101. Symmetric Tree 💚

递归三部曲:

  1. 确定递归函数的参数和返回值: 因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。返回值自然是bool类型。

  2. 确定终止条件:要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。 节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下称之为左节点右节点

    • 左节点为空,右节点不为空,不对称,return false
    • 左不为空,右为空,不对称 return false
    • 左右都为空,对称,返回true

    此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:

    • 左右都不为空,比较节点数值,不相同就return false

此时左右节点不为空,且数值也不相同的情况我们也处理了。

  1. 确定单层递归的逻辑:此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。

    • 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
    • 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
    • 如果左右都对称就返回true ,有一侧不对称就返回false 。
class Solution {
    public boolean isSymmetric(TreeNode root) {
        if (root == null){
            return true;
        }
        return isSymmetric(root.left,root.right);
        
    }
    
    public boolean isSymmetric(TreeNode left,TreeNode right){
        if(left == null && right == null){
            return true;
        }
        if(left != null && right == null){
            return false;
        }
        if(left == null && right != null){
            return false;
        }
        if (left.val != right.val){
            return false;
        }
        return isSymmetric(left.left,right.right)&&isSymmetric(left.right,right.left);
    }
}

3.1.2 相同的树 100. Same Tree 💚

思路和上一题一模一样

class Solution {
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == null && q == null){
            return true;
        }
        if (p == null && q != null){
            return false;
        }
        if (p != null && q == null){
            return false;
        }
        if (p.val != q.val){
            return false;
        }
        return isSameTree (p.left,q.left) && isSameTree(p.right,q.right);
    }
}

3.1.3 另一棵树的子树 572. Subtree of Another Tree 💚

本题和前面两题判断是否一样的思路是一致的,但是本题多了一个递归的逻辑,就是要判断子树。

class Solution {
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if(root == null){
            return false;
        }
        if(isSame(root,subRoot)){
            return true;
        }
        return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);   
    }
    
    public boolean isSame(TreeNode root, TreeNode subRoot){
        if(root == null && subRoot == null){
            return true;
        }
        if(root == null && subRoot != null){
            return false;
        }
        if(root != null && subRoot == null){
            return false;
        }
        if(root.val != subRoot.val){
            return false;
        }
        return isSame(root.left, subRoot.left) && isSame(root.right, subRoot.right);
    }
}

3.2 最大深度

3.2.1 二叉树的最大深度 104. Maximum Depth of Binary Tree 💚

  • DFS递归三部曲:
    1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型
    2. 确定终止条件:如果为空节点的话,就返回0,表示高度为0。
    3. 确定单层递归的逻辑:先求它的左子树的深度,再求的右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。
class Solution {
    public int maxDepth(TreeNode root) {
        if (root==null){
            return 0;
        }
        if (root.left != null && root. right == null){
            return maxDepth(root.left) + 1;
        }
        if (root.left == null && root. right != null){
            return maxDepth(root.right) + 1;
        }
        return Math.max (maxDepth(root.left), maxDepth(root.right))+ 1;

    }
}
  • BFS
class Solution{
    public int maxDepth(TreeNode root){
        int count = 0;
        Queue<TreeNode> queue = new LinkedList<>();
        if(root == null){
            return count;
        }
        queue.offer(root);
        while(!queue.isEmpty()){
            int len = queue.size();
            count++;
            for(int i = 0; i < len; i++){
                TreeNode temp = queue.poll();
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
            }
        }
        return count;
    }
}

3.2.2 n叉树的最大深度 559. Maximum Depth of N-ary Tree 💚

  • DFS
class Solution {
    public int maxDepth(Node root) {
        int depth = 0;
        if(root == null){
            return depth;
        }
        if(root.children != null){
            for(Node child : root.children){
                depth = Math.max(depth, maxDepth(child));
            }
        }
        return depth + 1;
    }
}
  • BFS
class Solution{
    public int maxDepth(Node root){
        int depth = 0;
        if(root == null){
            return depth;
        }
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            depth ++;
            int len = queue.size();
            for(int i = 0; i < len; i++){
                Node temp = queue.poll();
                List<Node> children = temp.children;
                for(Node child: children){
                    if(child != null){
                        queue.offer(child);
                    }
                }
            }
        }
        return depth;
    }
}

3.3 二叉树的最小深度 111. Minimum Depth of Binary Tree 💚

  • DFS递归三部曲:
    1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型
    2. 确定终止条件:如果为空节点的话,就返回0,表示高度为0。
    3. 确定单层递归的逻辑:如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。反之,右子树为空,左子树不为空,最小深度是 1 + 左子树的深度。 最后如果左右子树都不为空,返回左右子树深度最小值 + 1 。

容易犯的错误

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null){
            return 0;
        }
        int leftPath = minDepth(root.left);
        int rightPath = minDepth (root.right);
        if (root.left == null){
            return rightPath +1;
        }
        if(root.right ==null){
            return leftPath +1;
        }
        return Math.min(leftPath,rightPath) +1;
    }
}
  • BFS
class Solution{
    public int minDepth(TreeNode root){
        int res = 0;
        if(root == null){
            return res;
        }
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()){
            int len = queue.size();
            res++;
            for(int i = 0; i < len; i++){
                TreeNode temp = queue.poll();
                if(temp.left == null && temp.right == null){
                    return res;
                }
                if(temp.left != null){
                    queue.offer(temp.left);
                }
                if(temp.right != null){
                    queue.offer(temp.right);
                }
            }
        }
        return res;
    }
}

3.4 完全二叉树的节点个数 222. Count Complete Tree Nodes 🧡

要求:Time < O(n) 利用完全二叉树的特点: 完全二叉树只有两种情况,

  • 就是满二叉树:可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。
  • 最后一层叶子节点没有满:分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。

所以递归三部曲:

  1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以该节点为根节点二叉树的节点数量,所以返回值为int类型。
  2. 确定终止条件:如果为空节点的话,就返回0,表示节点数为0。判断其子树岂不是满二叉树,如果是则利用用公式计算这个子树(满二叉树)的节点数量,如果不是则继续递归. 判断依据就是左边的左边界和右边界的深度是不是一样。
  3. 确定单层递归的逻辑:先求它的左子树的节点数量,再求的右子树的节点数量,最后取总和再加一 (加1是因为算上当前中间节点)就是目前节点为根节点的节点数量。
int leftNum = getNodesNum(cur.left);      // 左
int rightNum = getNodesNum(cur.right);    // 右
int treeNum = leftNum + rightNum + 1;      // 中
return treeNum;

因为只判断左边的左边和右边的右边,中间的点并不会遍历。所以Time is O(log N * log N) < O(N)

class Solution {
    public int countNodes(TreeNode root) {
        if(root == null){
            return 0;
        }
        TreeNode left = root.left;
        TreeNode right = root.right;
        int leftdepth = 0;
        int rightdepth = 0;
        while(left != null){
            left = left.left;
            leftdepth++;
        }
        while(right != null){
            right = right.right;
            rightdepth++;
        }
        if(leftdepth == rightdepth){
            return(2 << leftdepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
            //且满二叉树的公式就是2^k - 1
        }
        return countNodes(root.left) + countNodes(root.right) + 1;
    }
}

3.5 平衡二叉树 110. Banlanced Binary Tree 💚

递归三部曲:

  1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以是不是平衡二叉树,boolean
  2. 确定终止条件:如果为空节点的话,就返回0,表示节点数为0。
  3. 确定单层递归的逻辑:如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则则返回-1,表示已经不是二叉平衡树了。
class Solution{
    public boolean isBalanced(TreeNode root){
        if(root == null){
            return true;
        }
        if(Math.abs(getDepth(root.left) - getDepth(root.right)) > 1){
            return false;
        }
        return isBalanced(root.left) && isBalanced(root.right);
    }
    
    public int getDepth(TreeNode root){
        if(root == null){
            return 0;
        }
        return Math.max(getDepth(root.left), getDepth(root.right)) + 1;
        
    }
}

3.6 二叉树的所有路径 257. Binary Tree Paths 💚

本题需要使用前序遍历,方便确定父子关系

前序遍历

递归三部曲

  1. 递归函数函数参数以及返回值:要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值
  2. 确定递归终止条件:因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。 那么什么时候算是找到了叶子节点?  是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。
  3. 确定单层递归逻辑: 因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。所以递归前要加上判断语句,下面要递归的节点是否为空。递归完,要做回溯啊,因为path 不能一直加入节点,它还要删节点,然后才能加入新的节点。
class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();
        if(root == null){
            return res;
        }
        traversal(root, path, res);
        return res;
    }
    
    public void traversal(TreeNode root, List<Integer> path, List<String> res){
        path.add(root.val);
        
        //叶子节点
        if(root.left == null && root.right == null){
            //output
            StringBuilder sb = new StringBuilder();
            //注意单独处理最后一个,因为前面所有数字后面都加箭头,唯独最后一个不加
            for(int i = 0; i < path.size() - 1; i++){
                sb.append(path.get(i)).append("->");
            }
            sb.append(path.get(path.size() - 1));
            res.add(sb.toString());
            return;
        }
        if(root.left != null){
            traversal(root.left, path, res);
            path.remove(path.size() - 1);// 回溯,打印完删除
        }
        if(root.right != null){
            traversal(root.right, path, res);
            path.remove(path.size() - 1);// 回溯,打印完删除
        }
    }
}

3.7左子叶之和 404. Sum of Left Leaves 💚

首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。

因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点

下图左子叶之和为0,因为没有左子叶!

左子叶==0

判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。 如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子。

递归三部曲:

  1. 确定递归函数的参数和返回值:判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int
  2. 确定终止条件: 如果遍历到空节点,则左子叶为0
  3. 确定单层递归的逻辑:当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和右子树左叶子之和,相加便是整个树的左叶子之和。
class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if(root == null){
            return 0;
        }
        int leftvalue = sumOfLeftLeaves(root.left);
        if(root.left != null && root.left.left == null && root.left.right == null){
            leftvalue = root.left.val;
        }
        int rightvalue = sumOfLeftLeaves(root.right);
        int res = leftvalue + rightvalue;
        return res;
    }
}

3.8 找树左下角的值 513. Find Bottom Left Tree Value 🧡

层序遍历,记录最下面一层,第一个树,就是整个树最左边的树。BFS经典套路

class Solution {
    public int findBottomLeftValue(TreeNode root) {
        int result = -1;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while(! queue.isEmpty()){
            result = queue.peek().val;
            int size = queue.size();
            for (int i= 0; i < size; i++){
                TreeNode top = queue.poll();
                if(top.left != null){
                    queue.offer(top.left);
                }
                if(top.right != null){
                    queue.offer(top.right);
                }
            }
        }
        return result;
        
    }
}

3.9 路径总和

3.9.1 路径总和 112. Path Sum 💚

递归三部曲:

  1. 确定递归函数的参数和返回类型: 参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

    再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

    • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
    • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先 (opens new window)中介绍)
    • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

    而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢? 图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。

112. path sum

  1. 确定终止条件:首先计数器如何统计这一条路径的和呢?不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。如果遍历到了叶子节点,count不为0,就是没找到。
  2. 确定单层递归的逻辑:因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null){
            return false;
        }
        targetSum = targetSum - root.val;
        if (root.left == null && root.right == null){
            return (targetSum == 0);
        }
        return hasPathSum(root.left,targetSum) ||hasPathSum(root.right,targetSum);
    }
}

3.9.2 路径总和II 113. Path Sum II 🧡

注意打印路径时,需要有一个回溯,即删除当前节点

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int sum) {
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) return res; // 非空判断

        List<Integer> path = new LinkedList<>();
        preorderdfs(root, sum, res, path);
        return res;
    }

    public void preorderdfs(TreeNode root, int targetsum, List<List<Integer>> res, List<Integer> path) {
        path.add(root.val);
        // 遇到了叶子节点
        if (root.left == null && root.right == null) {
            // 找到了和为 targetsum 的路径
            if (targetsum - root.val == 0) {
                res.add(new ArrayList<>(path));
            }
            return; // 如果和不为 targetsum,返回
        }

        if (root.left != null) {
            preorderdfs(root.left, targetsum - root.val, res, path);
            path.remove(path.size() - 1); // 回溯
        }
        if (root.right != null) {
            preorderdfs(root.right, targetsum - root.val, res, path);
            path.remove(path.size() - 1); // 回溯
        }
    }
}

4. 二叉树的修改与构造

4.1 翻转二叉树 226. Invert Binary Tree 💚

递归三部曲:

  1. 确定确定递归函数的参数和返回值: 参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为TreeNode
  2. 确定终止条件:节点为空就返回
  3. 确定单层递归逻辑: 因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。
class Solution {
    public TreeNode invertTree(TreeNode root) {
        //base case -return without recursion
        if (root==null){
            return null;
        }
        invertTree(root.left);
        invertTree(root.right);
        
        TreeNode temp = root.right;
        root.right = root.left;
        root.left = temp;
        return root;
    }
}

4.2 两个遍历构造二叉树

4.2.1 中序遍历和后序遍历构造二叉树 106. Construct Binary Tree from Inorder and Postorder Traversal 🧡

理论知识:以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

中+后 递归步骤:

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间
class Solution {
    Map<Integer, Integer> map;  // 方便根据数值查找位置
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) { // 用map保存中序序列的数值对应位置
            map.put(inorder[i], i);
        }

        return findNode(inorder,  0, inorder.length, postorder,0, postorder.length);  // 前闭后开
    }
    
    public TreeNode findNode(int[] inorder, int inBegin, int inEnd, int[] postorder, int postBegin, int postEnd) {
        // 参数里的范围都是前闭后开
        if (inBegin >= inEnd || postBegin >= postEnd) {  // 不满足左闭右开,说明没有元素,返回空树
            return null;
        }
        int rootIndex = map.get(postorder[postEnd - 1]);  // 找到后序遍历的最后一个元素在中序遍历中的位置
        TreeNode root = new TreeNode(inorder[rootIndex]);  // 构造结点
        int lenOfLeft = rootIndex - inBegin;  // 保存中序左子树个数,用来确定后序数列的个数
        root.left = findNode(inorder, inBegin, rootIndex,
                            postorder, postBegin, postBegin + lenOfLeft);
        root.right = findNode(inorder, rootIndex + 1, inEnd,
                            postorder, postBegin + lenOfLeft, postEnd - 1);

        return root;
    }
}

4.2.2 前序遍历和中序遍历构造二叉树 105. Construct Binary Tree from Preorder and Inorder Traversal 🧡

递归步骤:

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到前序数组第一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割前序数组,切成前序左数组和前序右数组
  • 第六步:递归处理左区间和右区间
class Solution {
    Map<Integer, Integer> map;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        map = new HashMap<>();
        for (int i = 0; i < inorder.length; i++) { // 用map保存中序序列的数值对应位置
            map.put(inorder[i], i);
        }

        return findNode(preorder, 0, preorder.length, inorder,  0, inorder.length);  // 前闭后开
    }

    public TreeNode findNode(int[] preorder, int preBegin, int preEnd, int[] inorder, int inBegin, int inEnd) {
        // 参数里的范围都是前闭后开
        if (preBegin >= preEnd || inBegin >= inEnd) {  // 不满足左闭右开,说明没有元素,返回空树
            return null;
        }
        int rootIndex = map.get(preorder[preBegin]);  // 找到前序遍历的第一个元素在中序遍历中的位置
        TreeNode root = new TreeNode(inorder[rootIndex]);  // 构造结点
        int lenOfLeft = rootIndex - inBegin;  // 保存中序左子树个数,用来确定前序数列的个数
        root.left = findNode(preorder, preBegin + 1, preBegin + lenOfLeft + 1,  
                            inorder, inBegin, rootIndex);
        root.right = findNode(preorder, preBegin + lenOfLeft + 1, preEnd,
                            inorder, rootIndex + 1, inEnd);

        return root;
    }
}

4.2.3 前序遍历和后序遍历构造二叉树:

前序遍历和后序遍历不能构造唯一确定的二叉树! 因为没有中序遍历无法确定左右部分,也就是无法分割。

example

tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。

tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。

4.3 最大二叉树 654. Maximum Binary Tree 🧡

过程展示:

最大二叉树 递归三部曲:

  1. 确定递归函数的参数和返回值:参数时传入存放元素的数组,返回构造二叉树的头节点,返回类型是指向节点的指针
  2. 确定终止条件:题目中说了输入的数组大小一定是大于等于1的,所以我们不用考虑小于1的情况,那么当递归遍历的时候,如果传入的数组大小小于1,说明遍历到了叶子节点了。返回空
  3. 确定单层递归逻辑:
    • 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组。
    • 用最大值创建新节点
    • 把该节点的左右分割,继续递归

4.4 合并二叉树 617. Merge Two Binary Trees 💚

递归三部曲:

  1. 确定递归函数的参数和返回值:首先那么要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。
  2. 确定终止条件:因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了啊(如果t2也为NULL也无所谓,合并之后就是NULL)。反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓,合并之后就是NULL)。
  3. 单层递归逻辑:单层递归的逻辑就比较好些了,这里我们用重复利用一下t1这个树,t1就是合并之后树的根节点(就是修改了原来树的结构)。那么单层递归中,就要把两棵树的元素加到一起。接下来t1 的左子树是:合并 t1左子树 t2左子树之后的左子树。t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树。最终t1就是合并之后的根节点。
class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if(root1 == null){
            return root2;
        }
        if(root2 == null){
            return root1;
        }
        root1.val = root1.val + root2.val;
        root1.left = mergeTrees(root1.left, root2.left);
        root1.right = mergeTrees(root1.right, root2.right);
        return root1;
    }
}

5. 二叉搜索树的属性

核心思想:二叉搜索树,中序遍历是有序数组!

5.1 二叉搜索树中的搜索 700. Search in a Binary Search Tree 💚

递归三部曲:

  1. 确定递归函数的参数和返回值:递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。
  2. 确定终止条件:如果root为空,或者找到这个数值了,就返回root节点。
  3. 确定单层递归的逻辑:因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。如果root.val > val,搜索左子树,如果root.val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if(root == null || root.val == val){
            return root;
        }
        if(root.val > val){
            return searchBST (root.left, val);
        }else {
            return searchBST(root.right, val);
        }
    }
}

5.2 验证二叉搜索树 98. Validate Binary Search Tree 🧡

二叉搜索树特征:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。
  • ⚠️:等于也是false

中序遍历下,输出的二叉搜索树节点的数值是有序序列。 有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。

思路:记录中序遍历的结果,再验证结果是不是有序序列。

class Solution{
    public boolean isValidBST(TreeNode head){
        List<TreeNode> res = new ArrayList<>();
        process(head, res);
        for (int i = 0; i < res.size() - 1; i++){
            if(res.get(i).val >= res.get(i+1).val){
                return false;
            }
        }
        return true;
    }
    
    public void process(TreeNode head, List<TreeNode> res){
        if(head == null){
            return;
        }
        process(head.left, res);
        res.add(head);
        process(head.right, res);
    }
}

5.3 二叉搜索树的最小绝对差 530. Minimum Absolute Difference in BST 💚

二叉搜索树采用中序遍历,其实就是一个有序数组。

在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。

最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了。

需要用一个pre节点记录一下cur节点的前一个节点。

中序遍历中的pre节点

class Solution {
    int res = Integer.MAX_VALUE;
    TreeNode pre;
    public int getMinimumDifference(TreeNode root) {
        if(root == null){
            return 0;
        }
        process(root);
        return res;
    }
    
    public void process(TreeNode root){
        if(root == null){
            return;
        }
        process(root.left);
        if(pre != null){
            res = Math.min(res, root.val - pre.val);
        }
        pre = root;
        process(root.right);
    }
}

5.4 二叉搜索树中的众数 501. Find Mode in Binary Search Tree 💚

如果不是搜索二叉树:

  1. 遍历整个树,用map统计频率
  2. 把统计出来的频率(map中的value)排序
  3. 取前面的高频元素

但二叉搜索树,中序遍历是有序的。遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。

弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。

此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办?应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个)这种方式遍历了两遍数组。

那么如何只遍历一遍呢?

如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中。频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集,因为结果集之前的元素都失效了。

//Time: O(n)
class Solution {
    ArrayList<Integer> resList;
    int count;
    int maxcount;
    TreeNode pre;
    
    public int[] findMode(TreeNode root) {
        if(root == null){
            return null;
        }
        resList = new ArrayList<>();
        count = 0;
        maxcount = 0;
        process(root);
        int[] res = new int[resList.size()];
        for(int i = 0; i < resList.size(); i++){
            res[i] = resList.get(i);
        }
        return res;
        
    }
    
    public void process(TreeNode root){
        if(root == null){
            return;
        }
        process(root.left);
        int temp = root.val;
        if(pre == null || temp != pre.val){
            count = 1;
        }else{
            count++;
        }
        if(count > maxcount){
            resList.clear();
            resList.add(temp);
            maxcount = count;
        }else if(count == maxcount){
            resList.add(temp);
        }
        pre = root;
        process(root.right);
    }
}

5.5 把二叉搜索树转换为累加树 538. Convert BST to Greater Tree 🧡

二叉搜索树啊,这是有序的啊。

那么有序的元素如果求累加呢?

其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。

为什么变成数组就是感觉简单了呢?

因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。

那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了

class Solution {
    int sum;
    public TreeNode convertBST(TreeNode root) {
        sum = 0;
        process(root);
        return root;
    }
    public void process(TreeNode root){
        if(root == null){
            return;
        }
        process(root.right);
        sum += root.val;
        root.val = sum;
        process(root.left);
    }
}

6. 二叉搜索树的修改与构造

6.1 二叉搜索树中的插入 701. Insert into a Binary Search Tree 🧡

思路:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。 二叉搜索树插入

例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,需要调整二叉树的结构么? 并不需要。 。只要遍历二叉搜索树,找到空节点 插入元素就可以了,那么这道题其实就简单了。

使用一个pre指针,记录父节点位置。遍历二叉搜索树,直到找到空位插入,然后判断插入在父节点的左边还是右边.

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if(root == null){
            return new TreeNode(val);
        }
        TreeNode Newroot = root;
        TreeNode pre = root;
        while(root != null){
            pre = root;
            if(val > root.val){
                root = root.right;
            }else{
                root = root.left;
            }
        }
        if(val > pre.val){
            pre.right = new TreeNode(val);
        }else{
            pre.left = new TreeNode(val);
        }
        return Newroot;
    }
}

6.2 删除二叉搜索树中的节点 450. Delete Node in a BST 🧡

递归三部曲:

  1. 确定递归函数参数以及返回值:通过递归返回值删除节点。input 节点和删除值,output删除的点。
  2. 确定终止条件:遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了。
  3. 确定单层递归的逻辑:有5种情况:
    • 没有找到删除节点
    1. 遍历到空节点就返回
    • 找到删除节点
    1. 左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    2. 删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    3. 删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    4. 左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。

情况5解释

动画中棵二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。要删除的节点(元素7)的右孩子(元素9)为新的根节点。

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        root = delete(root,key);
        return root;
    }

    private TreeNode delete(TreeNode root, int key) {
        // 第一种情况:没找到删除的节点,遍历到空节点直接返回
        if(root == null){
            return null;
        }
        if(key > root.val){//还没找到,根据值遍历搜索二叉树
            root.right = delete(root.right, key);
        }else if(key < root.val){
            root.left = delete(root.left, key);
        }else{
            // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
            if(root.left == null && root.right == null){
                return null;
            }else if(root.left == null){ // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
                return root.right;
            }else if(root.right == null){// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
                return root.left;
            }else{
            // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
            // 并返回删除节点右孩子为新的根节点。
                TreeNode temp = root.right;// 找右子树最左面的节点
                while(temp.left != null){
                    temp = temp.left;
                }
                temp.left = root.left;// 把要删除的节点(root)左子树放在cur的左孩子的位置
                root = root.right;// 返回旧root的右孩子作为新root
                return root;
            }
        }
        return root;
    }
}

6.3 修剪二叉搜索树 669. Trim a Binary Search Tree 🧡

  1. 确定递归函数的参数以及返回值: 返回TreeNode
  2. 确定终止条件:修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了
  3. 确定单层递归逻辑:
    • 如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。
    • 如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点
    • 接下来要将下一层处理完左子树的结果赋给root.left,处理完右子树的结果赋给root.right。
    • 最后返回root节点
class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if(root == null){
            return root; 
        }
        root = trim(root, low, high);
        return root;
    }
    
    public TreeNode trim(TreeNode root, int low, int high){
        
        if(root == null){
            return null;
        }
        if(root.val < low){
            return trim(root.right, low, high);
        }else if(root.val > high){
            return trim(root.left, low, high);
        }else{
            root.left = trim(root.left, low, high);
            root.right = trim(root.right, low, high);
        }
        
        return root;
    }
}

6.4 将有序数组转换为二叉搜索树 108. Convert Sorted Array to Binary Search Tree 💚

本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间

那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个?

取哪一个都可以,只不过构成了不同的平衡二叉搜索树。

例如:输入:[-10,-3,0,5,9]

如下两棵树,都是这个数组的平衡二叉搜索树:

不同情况

如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。

这也是题目中强调答案不是唯一的原因。 递归三部曲:

  1. 确定递归函数返回值及其参数:用递归函数的返回值来构造中节点的左右孩子。我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间
  2. 确定递归终止条件:这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。
  3. 确定单层递归逻辑:取了中间位置,就开始以中间位置的元素构造节点。接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。最后返回root节点。
class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        TreeNode root = process(nums, 0, nums.length - 1);
        return root;
    }
    public TreeNode process(int[] nums, int low, int high){
        if(low > high){
            return null;
        }
        int mid = low + (high - low)/2;
        TreeNode root = new TreeNode(nums[mid]);
        root.left = process(nums, low, mid - 1);
        root.right = process(nums, mid + 1, high);
        return root;
    }
}

7. 二叉树的公共祖先问题

7.1 二叉树的最近公共祖先 236. Lowest Common Ancestor of a Binary Tree 🧡

递归三部曲:

  1. 确定递归函数返回值以及参数:需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。
  2. 确定终止条件:遇到空的话,然后然后空,因为树都是空了,所以返回空。那么我们来说一说,如果 root == q,或者 root == p,说明找到 q p ,则将其返回,这个返回值,后面在中节点的处理过程中会用到,那么中节点处理逻辑,后下面讲解。
  3. 确定单层递归逻辑: 在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)

最小公共祖先

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q){
        if(root == null || root == p || root == q){
            return root;
        }
        root.left = lowestCommonAncestor(root.left, p, q);
        root.right = lowestCommonAncestor(root.right, p, q);
        if(root.left == null && root.right == null){
            return null;
        }else if(root.left == null){
            return root.right;
        }else if(root.right == null){
            return root.left;
        }else{
            return root;
        }
    }
}

7.2 二叉搜索树的最近公共祖先 235. Lowest Common Ancestor of a Binary Search Tree 🧡

从上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root.val > p.val && root.val > q.val){
            return lowestCommonAncestor(root.left, p, q);
        }
        if(root.val < p.val && root.val < q.val){
            return lowestCommonAncestor(root.right, p, q);
        }
        return root;
    }
}