代码随想录 二叉树模块小结(1)

二叉树这块内容还是比较多的,但是模板也比较固定,总的来说难度不算太大。

个人认为二叉树难点主要有两个

1、遍历顺序

2、递归返回值以及处理方式

根据随想录的总结思路,分成以下几个模块

1、二叉树的遍历方式

2、二叉树的属性

3、二叉树的修改与构造

4、二叉搜索树的属性

5、二叉搜索树的修改与构造

6、公共祖先问题

目前想法是,二叉树与二叉搜索树各分开写一篇,公共祖先问题另写一篇。

那么这篇就是写普通二叉树的思路总结与方法。

目录

一、二叉树的遍历方式

1.1、前序遍历

1.2、后序遍历

1.3、中序遍历

1.4、层序遍历

二、二叉树的属性

2.1、二叉树是否对称

2.2、深度问题

2.2.1、最大深度

2.2.2、最小深度

2.3、二叉树节点个数

2.3.1、普通二叉树节点个数

2.3.2、完全二叉树节点个数

2.4、是否平衡

2.5、回溯与路径问题

2.5.1、所有路径

2.5.2、找树左下角的值

2.5.3、路径总和

 三、二叉树的修改与构造

3.1、二叉树翻转

3.2、二叉树构造

3.3、二叉树合并


一、二叉树的遍历方式

二叉树的遍历方式有四种,前序、中序、后序遍历以及层序遍历。

其中前三种遍历方式关键就是中节点处理顺序,递归方式和迭代方式都是一样的。

递归比较简单,只是调换中节点的处理顺序就可以了,这里只记录迭代方式。

1.1、前序遍历

遍历顺序为中左右,中节点是最先遍历的。

因为节点的遍历顺序和处理顺序是不一致的,所以要使用栈保存节点。

代码如下

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        Stack<TreeNode> s = new Stack();
        ArrayList<Integer> res = new ArrayList();  // 保存结果
        if (root != null) s.push(root);
        while (!s.isEmpty()) { 
            TreeNode node = s.pop();  // 处理中节点 即根节点
            res.add(node.val);
            if (node.right != null) s.push(node.right);  // 先放右节点 因为右节点最后出栈
            if (node.left != null) s.push(node.left);
        }
        return res;
    }
}

1.2、后序遍历

后序遍历为左右中,即中右左的反向,因此可以将前序遍历的左右孩子入栈顺序交换,再reverse一下结果集就可以了。

代码如下

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        Stack<TreeNode> s = new Stack();
        ArrayList<Integer> res = new ArrayList();
        if (root != null) s.push(root);
        while (!s.isEmpty()) {
            TreeNode node = s.pop();
            res.add(node.val);
            if (node.left != null) s.push(node.left);    // 调换一下左右孩子入栈顺序
            if (node.right != null) s.push(node.right);
        }
        Collections.reverse(res);   // 反转结果集
        return res;
    }
}

1.3、中序遍历

中序遍历为左中右,与前后序的代码逻辑不同。

得先遍历所有左节点,一旦遇到空节点则当前节点为根节点,处理完后再遍历右节点。

代码如下

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        Stack<TreeNode> s = new Stack();
        ArrayList<Integer> res = new ArrayList();
        TreeNode node = root;
        while (!s.isEmpty() || node != null) {
            while (node != null) {
                s.push(node);
                node = node.left;   // 左
            }
            node = s.pop();
            res.add(node.val);     // 中
            node = node.right;     // 右
        }
        return res;
    }
}

1.4、层序遍历

层序遍历是一层层往下遍历,每一层的顺序是从左往右。

层序遍历使用队列实现,要求每处理一个节点,就将其左右节点放入,并将处理的当前节点弹出。

代码如下

class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        Queue<TreeNode> queue = new LinkedList();
        List<List<Integer>> res = new ArrayList();
        if (root != null) queue.add(root);
        while (!queue.isEmpty()) {
            int size = queue.size();   // 这里是每一层的节点个数
            ArrayList<Integer> temp = new ArrayList();
            while (size > 0) {   // 处理每一层节点
                TreeNode node = queue.remove();  // 当前节点
                temp.add(node.val);
                if (node.left != null) queue.add(node.left);  // 处理完当前节点 放左右孩子进入队列
                if (node.right != null) queue.add(node.right);
                size--;
            }
            res.add(new ArrayList(temp));
        }
        return res;
    }
}

二、二叉树的属性

2.1、二叉树是否对称

对应题目101. 对称二叉树 - 力扣(LeetCode)

看二叉树是否对称,其实就是看左右子树是否可以相互翻转,或者说具体一点,左、右子树的外层与左、右子树的内层是否相等,如果完全相等则可以相互翻转。

那么思路就变成了同时遍历左右子树的外层和内层,如果都相等那么对称,有任何一个节点不相等则不对称。

根据递归三部曲,首先

1、确定递归的返回值和参数,因为一个节点要知道下层节点,即孩子是否是对称的,如果孩子不对称,那么无论当前节点的比较结果如何,都向上返回告知上层节点,因此递归返回值为boolean.

要同时遍历两棵树,则参数为root1和root2.

2、确定递归终止条件,本道题有两个终止条件,即当前节点为空,或者孩子不构成对称,那么可以直接向上返回

3、遍历顺序,这道题其实都可以,但优先前序遍历,因为根据终止条件,自顶向下是可以剪枝的。

代码如下

class Solution {
    public boolean isSymmetric(TreeNode root) {
        if (root == null) return true;
        return traversal(root.left, root.right);
    }

    public boolean traversal(TreeNode left, TreeNode right) {

        // 四种情况 已经包含了递归终止条件
        // 先判断当前节点
        // 都为空 返回true
        if (left == null && right == null) return true; 
        // 只有其中一个为空 返回false
        else if (left != null && right == null || right != null && left == null) return false;
        // 都不为空 数值不相等的话 返回false
        else if (left != null && right != null && left.val != right.val) return false;
        // 都不为空 看看子树是否对称
        boolean outV = traversal(left.left, right.right);
        boolean inV = traversal(left.right, right.left);
        return outV && inV;
    }
}

2.2、深度问题

树中的深度和高度的概念:

  • 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
  • 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数后者节点数(取决于高度从0开始还是从1开始)

 通俗点说,深度是从上往下数的,类似于从地面向下延伸,高度是从下往上数的,类似于从地面向上生长。

那么显然,根节点的深度为1,根节点的高度为树的层数。

理解了这些,做题就不会搞混,并且可以确定遍历顺序。

2.2.1、最大深度

对应题目104. 二叉树的最大深度 - 力扣(LeetCode)

最大深度其实就是树的高度,即从最底层的节点往上数,一直到根节点的层数。

那么遍历顺序就是后序遍历了,即自底向上。

递归三部曲:

1、递归返回值和参数,由于下层节点要告诉本层节点它的深度信息,本层节点也要向上返回本层的节点信息,因此返回值类型为int,遍历一棵树,参数为root.

2、终止条件,遇到空节点就返回深度为0

3、遍历顺序,后序遍历。

代码如下

class Solution {
    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        int leftD = maxDepth(root.left);  // 左子树的深度
        int rightD = maxDepth(root.right);  // 右子树的深度
        return Math.max(leftD, rightD) + 1;  // + 1是算上本节点的深度 向上返回
    }
}

2.2.2、最小深度

对应题目111. 二叉树的最小深度 - 力扣(LeetCode)

最小深度,其实就是从根节点出发到层数最近的叶子结点的层数,注意是叶子结点。

此时可以用前序遍历,但依然可以使用后序遍历,只要获取左右节点深度的最小值就可以了。

不过这道题相比最大深度有一个坑,两次我都踩进去了,即不能像最大深度一样,直接返回左右节点深度的最小值,即

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) return 0;
        int leftD = minDepth(root.left);
        int rightD = minDepth(root.right);
        
        return Math.min(leftD, rightD) + 1;  // 中 
    }
}

这样写是错误的,因为会忽略一种情况

 此时如果直接返回左右孩子的深度最小值,就会直接返回1,导致错误答案。

所以还要加入约束条件,即当前节点左孩子或右孩子为空时,返回不为空的孩子的深度加上本节点的深度。

正确代码如下

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) return 0;
        int leftD = minDepth(root.left);
        int rightD = minDepth(root.right);
        // 一定要记得加上本节点的深度
        if (root.left == null && root.right != null) return rightD + 1; // 左为空 右不为空
        if (root.right == null && root.left != null) return leftD + 1;  // 右为空 左不为空
        return Math.min(leftD, rightD) + 1;  // 中 处理了左右都为空或都不为空的情况
    }
}

2.3、二叉树节点个数

2.3.1、普通二叉树节点个数

如果是普通二叉树的节点个数,直接递归遍历统计就行了。

递归返回值为int,因为要告知上层节点本层节点的节点个数。

遍历顺序无关紧要,哪种都可以。

代码如下

class Solution {

    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        // 返回左右孩子加上当前节点的节点个数
        return countNodes(root.left) + countNodes(root.right) + 1;  
    }
}

2.3.2、完全二叉树节点个数

完全二叉树除了底层节点外,其余层节点都是满的。

满二叉树可以有公式计算节点个数

若满二叉树层数为h,则节点个数为2^h - 1,底层节点个数为2^(h-1)。

而满二叉树是完全二叉树的特例,那么完全二叉树中一定会有满二叉树

因此如果遍历到一个节点是满二叉树的根节点,那么可以直接用公式计算节点个数,其下面的节点就不用再遍历了,可以提高效率。

那么如何判断一棵树是满二叉树?方法就是直接遍左右子树的外层节点,看深度是否相等,如果相等则一定是满二叉树。

是满二叉树:

 不是满二叉树:

代码如下

class Solution {
    public int countNodes(TreeNode root) {

        // 完全二叉树节点个数解法 效率更高
        if (root == null) return 0;
        TreeNode leftNode = root.left;
        TreeNode rightNode = root.right;
        int leftD = 0;  // 初始化为0  方便后面指数计算
        int rightD = 0;
        // 当遇到满二叉树的话 可以直接通过公式把该满二叉树的节点算出来 就不用遍历它下面的孩子节点了
        while (leftNode != null) {
            leftD++;
            leftNode = leftNode.left;
        }
        while (rightNode != null) {
            rightD++;
            rightNode = rightNode.right;
        }
        // 完全二叉树 左右子树外层深度相同 就是满二叉树 那么直接计算并返回
        if (leftD == rightD) {
            // 2 << leftD 其实就是2^(leftD + 1)  比如leftD为2 那么节点数为2^(2+1)
            return (2 << leftD) - 1;  
        }
        // 不是满二叉树 老老实实递归
        return countNodes(root.left) + countNodes(root.right) + 1;
    }
}

2.4、是否平衡

对应题目110. 平衡二叉树 - 力扣(LeetCode)

这道题要抓住判断一棵树是否是平衡树的两个关键点:

1、左右子树都是平衡树

2、左右子树高度差小于等于1

满足以上两点,才能是平衡树,换句话说,有一点不满足,那么就不是平衡树。

递归三部曲:

1、递归返回值和参数,由于要获知孩子的高度差信息同时也要获知孩子是否是平衡树,必须使用Int,当孩子是平衡树时,返回其高度,不是平衡树时返回-1,个人认为这个点是比较难想到的点,如果想到了就会比较好做。参数为root。

2、终止条件,遇到空节点就返回高度为0

3、遍历顺序,由于要获取左右孩子的高度来做后续处理,所以使用后序遍历,自底向上。

代码如下

class Solution {
    public boolean isBalanced(TreeNode root) {
        return traversal(root) != -1;
    }

    // 该节点为平衡树 返回高度
    // 该节点不为平衡树 返回-1
    public int traversal(TreeNode root) {
        if (root == null) return 0;
        int leftD = traversal(root.left);   // 左子树高度
        int rightD = traversal(root.right); // 右子树高度
        // -1表示该节点为根的树不为平衡树
        if (leftD == -1 || rightD == -1) return -1;  // 左右子树有一个不为平衡树
        if (Math.abs(leftD - rightD) > 1) return -1;  // 左右子树高度差大于1
        return Math.max(leftD, rightD) + 1;  //该节点为根的树是平衡树 返回树的高度
    }
}

2.5、回溯与路径问题

回溯其实就是在调用完函数后,使某一变量回到调用之前的状态,即该变量的信息与调用函数绑定。

而递归完后会有函数回退的过程,这就是天然的回溯过程。

从具体题目来看回溯会更清晰一些。

2.5.1、所有路径

对应题目257. 二叉树的所有路径 - 力扣(LeetCode)

这道题要搜集树的所有路径,即自顶向下搜索。

在遍历树的过程中搜集路径,一旦到了叶子节点,就收集路径到结果集。

那么搜集完一个路径后,如何回退?此时就要回溯了

        // 递归回溯一定要放一起
        if (root.left != null) {
            traversal(root.left, path);
            path.remove(path.size() - 1);  // 回溯过程
        }
        if (root.right != null) {
            traversal(root.right, path);
            path.remove(path.size() - 1);
        }

当调用完traversal(root.left, path)后,path路径添加了一个元素,此时要保持与调用之前一样的状态,就得将该path回退,即去除添加后的那个元素。

递归与回溯总是一起的,即递归完后一定要跟着回溯。

参数path可以作为全局变量,也可以作为参数传递。

代码如下

class Solution {
    ArrayList<String> res = new ArrayList();  // 保存结果
    public List<String> binaryTreePaths(TreeNode root) {
        ArrayList<Integer> path = new ArrayList();  // 保存路径
        traversal(root, path);
        return res;
    }

    public void traversal(TreeNode root, ArrayList<Integer> path) {
        path.add(root.val);  // 中 一定要先放 遍历到一个就放一个
        // 到叶子结点  收集路径
        if (root.left == null && root.right == null) {
            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);
            path.remove(path.size() - 1);  // 回溯过程
        }
        if (root.right != null) {
            traversal(root.right, path);
            path.remove(path.size() - 1);
        }
    }
}

2.5.2、找树左下角的值

对应题目513. 找树左下角的值 - 力扣(LeetCode)

这道题的思路就是通过前序遍历找最底层的节点,一旦找到深度最大的节点,那么该节点一定是最左边的节点。

递归三部曲:

1、递归返回值,直接找深度最大的节点,不需要返回值;参数为root

2、终止条件,遇到空节点就返回

3、遍历顺序,前序遍历

记录一个全局最大深度与深度的计数值,一旦深度的计数值大于最大深度,说明找到了节点。

代码如下

class Solution {
    int maxDepth = Integer.MIN_VALUE;
    int res = 0;
    int depth = 0;
    public int findBottomLeftValue(TreeNode root) {
        // 递归回溯解决  中序遍历同时记录最大深度  深度最大的一定是最左边的
        traversal(root);
        return res;
    }

    public void traversal(TreeNode node) {
        if (node == null) return;
        if (depth > maxDepth) {
            maxDepth = depth; // 不断更新最大深度
            res = node.val;   // 记录该节点的值
        }
        depth += 1;
        traversal(node.left);
        depth -= 1;

        depth += 1;
        traversal(node.right);
        depth -= 1;
    }
}

其实这样写代码有些冗余,完全可以将depth作为参数进行传递,即回溯的另一种写法

class Solution {
    int maxDepth = Integer.MIN_VALUE;
    int res = 0;
    public int findBottomLeftValue(TreeNode root) {
        // 递归回溯解决  中序遍历同时记录最大深度  深度最大的一定是最左边的
        traversal(root, 0);
        return res;
    }

    public void traversal(TreeNode node, int depth) {
        if (node == null) return;
        if (depth > maxDepth) {
            maxDepth = depth; // 不断更新最大深度
            res = node.val;   // 记录该节点的值
        }
        traversal(node.left, depth + 1);  // 包含回溯过程
        traversal(node.right, depth + 1);
    }
}

每次传入的depth + 1,由于作为形参传递,并不会影响depth本身的值,所以每次回到该函数,depth的值是不变的,相当于回溯过程了。

2.5.3、路径总和

对应题目112. 路径总和 - 力扣(LeetCode)

这道题其实和所有路径很像,都是到叶子结点进行处理。

这道题思路是遍历时累加节点数值,到叶子结点判断是否和目标值相等。

递归三部曲:

1、递归返回值类型和参数,要告诉上层节点,该节点的叶子结点是否包含路径总和只要左右孩子有一个满足条件,向上返回true。因此返回值为boolean类型。参数为root,targetSum,res。累加数值res直接作为参数进行传递和回溯。

2、终止条件,遇到叶子就直接判断是否等于目标和

3、遍历顺序,要处理左右孩子的返回结果,后序遍历

代码如下

class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) return false;
        return traversal(root, targetSum, root.val);
    }

    public boolean traversal(TreeNode root, int targetSum, int res) {
        if (root.left == null && root.right == null) {
            return res == targetSum;   // 到叶子 判断和是否相等
        }
        boolean leftR = false;
        boolean rightR = false;
        if (root.left != null) {  // 左
            leftR = traversal(root.left, targetSum, res + root.left.val);  // 回溯
        }
        if (root.right != null) {  // 右
            rightR = traversal(root.right, targetSum, res + root.right.val); // 回溯
        }
        return leftR || rightR;   // 中  左右子树返回结果只要有一个为true 表示找到了路径 向上返回true
    }
}

 三、二叉树的修改与构造

这部分二叉树的修改,包含三个部分:

1、二叉树翻转

2、二叉树构造

3、二叉树合并

3.1、二叉树翻转

对应题目226. 翻转二叉树 - 力扣(LeetCode)

这道题其实思路比较好想到,就是直接反转左右孩子,最后达到整棵树反转的目的。

比较简单,递归不需要返回值,遍历顺序可以选择前序、后序或者层序。

但不要用中序,因为中序遍历是左中右,因为反转中节点之后,原本的右子树变成了未反转中节点前的左子树,而原先未反转中节点的左子树已经反转过了,不能再反转,因此逻辑上实现太别扭。

递归实现比较简单,给出迭代版的代码

层序遍历:

class Solution {
    public TreeNode invertTree(TreeNode root) {
        // 可以前 后序 唯独不能中序 
        // 中序的话 遍历的右子树实际上是原来的左子树 而左子树已经反转过了 所以要反转两次左子树
        Stack<TreeNode> s = new Stack();
        if (root != null) s.push(root);
        while (!s.isEmpty()) {
            TreeNode node = s.pop();
            swap(node);   // 交换左右孩子
            if (node.right != null) s.push(node.right);
            if (node.left != null) s.push(node.left);
        }
        return root;
    }

    // 交换左右孩子
    public void swap(TreeNode node) {
        TreeNode temp = node.left;
        node.left = node.right;
        node.right = temp;
    }
}

3.2、二叉树构造

对应题目106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

这种题目比较死板,直接记住步骤就可以做

1、以后序数组的最后一个元素作为根节点,如果是前序数组就找第一个元素,之后找该元素在中序数组的下标index作为分割点

2、以分割点创建根节点

3、对中序数组进行分割,分成左右子数组(不包括已经创建的节点元素),注意数组的边界,一般左闭右开。

4、对后序数组进行分割,后序左数组的右边界等于后序左边界 + (中序左数组的个数 + 1),后序右数组就取剩下的元素(不包括已经创建的节点元素)

5、递归创建左子树和右子树

递归三部曲:

1、递归返回值和参数,要获取左右子树,因此返回值为TreeNode,参数为中序和后序数组,以及其左右数组的边界,以边界作为参数进行子树的构建。

2、终止条件,①当数组边界出现异常,即越界时,返回空节点;②只剩一个元素,确定是叶子节点,直接返回当前节点

3、遍历顺序,前序遍历。

代码如下

class Solution {
    // 需要map辅助定位中序数组的下标
    HashMap<Integer, Integer> map = new HashMap();
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        // 映射下标
        for (int i = 0; i < inorder.length; i++) map.put(inorder[i], i);
        // 左闭右开
        return traversal(inorder, 0, inorder.length, postorder, 0, postorder.length);
    }

    public TreeNode traversal(int[] inorder, int inorderBegin, int inorderEnd, int[] postorder, int postorderBegin, int postorderEnd) {
        // 判断是否越界
        if (postorderEnd == postorderBegin) return null;
        // 创建当前节点
        TreeNode root = new TreeNode(postorder[postorderEnd - 1]);  // 中逻辑
        // 判断是否为叶子 是叶子直接返回当前节点
        if (postorderEnd - postorderBegin == 1) return root;

        // 寻找分割点
        int delimiterIndex = map.get(postorder[postorderEnd - 1]);

        // 分割中序数组和后序数组  左闭右开
        // 分割中序数组
        int leftInorderBegin = inorderBegin;
        int leftInorderEnd = delimiterIndex;
        int rightInorderBegin = delimiterIndex + 1;  // 这个节点选过了 不能再选
        int rightInorderEnd = inorderEnd;

        // 分割后序数组
        int leftPostorderBegin = postorderBegin;
        int leftPostorderEnd = postorderBegin + leftInorderEnd - leftInorderBegin;
        int rightPostorderBegin = leftPostorderEnd;
        int rightPostorderEnd = postorderEnd - 1;    // 这个节点选过了 不能再选

        // 递归构建左右子树
        root.left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder,  leftPostorderBegin, leftPostorderEnd);  // 左
        root.right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd);  // 右
        return root;
    }
}

3.3、二叉树合并

对应题目617. 合并二叉树 - 力扣(LeetCode)

这道题也比较简单,主要是分三种情况。

1、两棵树的节点都为空,直接返回空。

2、两棵树节点有一个为空,返回不为空的节点。

3、都不为空,数值相加并返回节点。

递归三部曲:

1、递归返回值和参数,要获取左右子树,因此返回值为TreeNode,要遍历两棵树,参数为root1,root2

2、终止条件,三种情况其实都可以作为终止条件了。

3、遍历顺序,中的处理逻辑不需要孩子的返回值来做后续处理,因此前序和后序都行。

代码如下

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null) return root2;  // 有一个为空 返回另一个 包含了两个都为空的情况
        if (root2 == null) return root1;
        root1.val += root2.val;   // 中逻辑  两个都不为空的情况
        root1.left = mergeTrees(root1.left, root2.left);  // 左
        root1.right = mergeTrees(root1.right, root2.right);  // 右
        return root1;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值