刷题总结 | 二叉树总结篇

202 阅读10分钟

二叉树总结篇

二叉树就不多介绍了,但完全二叉树,二叉搜索树,平衡二叉树,B树,B+树等概念还是应该明确。

二叉树的遍历方式可大致分为递归法和迭代法。又可以根据遍历节点的顺序分成前、中、后序遍历。

二叉树的种类

完全二叉树

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

image.png

如果最底层节点也全都填满了,那么这是一颗 满二叉树。

二叉搜索树

二叉搜索树是一个有序树。

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

用中序遍历可以将二叉搜索树转换成有序数组。

image.png

平衡二叉搜索树

二叉搜索树的平均查找时间复杂度为O(logn),但是二叉搜索树是有可能退化为链表的。如何保证它不退化呢?

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

image.png

二叉树的遍历方式

二叉树主要有两种遍历方式:

  • 深度优先遍历:先往深走,遇到叶子节点再往回走。
    • 前序遍历(递归法,迭代法)
    • 中序遍历(递归法,迭代法)
    • 后序遍历(递归法,迭代法)
  • 广度优先遍历:一层一层的去遍历。
    • 层次遍历(迭代法)

这里仅以前序遍历作一个例子,中序遍历和后序遍历只是换了顺序。

递归法

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new LinkedList<>();
        traversal(root, res);
        return res;
    }   
    public void traversal(TreeNode root, List<Integer> res) {
        if(root == null) return;
        res.add(root.val); // 中
        traversal(root.left, res); // 左
        traversal(root.right, res); // 右
    }
}

迭代法,用栈来模拟递归,添加元素是右左中,这样出栈的顺序就是 中 左 右。

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

层序遍历,用队列来遍历每一层的元素。

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

二叉树的性质

求深度是个看起来很简单的问题,这里要注意最小深度指的是叶子节点到根节点的路径。

这类问题都可以先求左边深度,再求右边深度,最后比较得到结果然后return给上层的节点。

实在理不清递归的思路,可以用层序遍历法,很好用!

104. 二叉树的最大深度

class solution {
    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);
        return Math.max(leftDepth, rightDepth) + 1;
    }
}

111. 二叉树的最小深度

class Solution {
    /**
     * 递归法,相比求MaxDepth要复杂点
     * 因为最小深度是从根节点到最近**叶子节点**的最短路径上的节点数量
     */
    public int minDepth(TreeNode root) {
        if (root == null) return 0;
        int leftDepth = minDepth(root.left);
        int rightDepth = minDepth(root.right);
        if (root.left == null) return rightDepth + 1;
        if (root.right == null) return leftDepth + 1;
        // 左右结点都不为null
        return Math.min(leftDepth, rightDepth) + 1;
    }
}

101. 对称二叉树

这道题的思路比较简单,但我一开始没想到。可以用两个指针q,p来遍历左子树和右子树。如果是完全对称的二叉树,其左子树应该和右子树相同。

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

226. 翻转二叉树

class Solution {
   /**
     * 前后序遍历都可以,层序也好用
     */
    public TreeNode invertTree(TreeNode root) {
        if (root == null) {
            return null;
        }
        invertTree(root.left);
        invertTree(root.right);
        swapChildren(root);
        return root;
    }

    private void swapChildren(TreeNode root) {
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
    }
}

116. 填充每个节点的下一个右侧节点指针

class Solution {
    public Node connect(Node root) {
        if(root == null) return root;
        Node cur = root;
        Queue<Node> q = new LinkedList<>();
        q.offer(root);
        while(!q.isEmpty()){
            int len = q.size();
            Stack<Node> s= new Stack<>();
            while(len-- > 0){
                Node node = q.poll();
                if(node.left != null) q.offer(node.left);
                if(node.right != null) q.offer(node.right);
                if(!s.isEmpty()){
                    Node n = s.peek();
                    n.next = node;
                }
                s.push(node);
            }
        }
        return root;
    }
}

222. 完全二叉树的节点个数

递归法(后序遍历)

class Solution {
    public int countNodes(TreeNode root) {
        if(root == null) return 0;
        int left = countNodes(root.left);
        int right = countNodes(root.right);
        return left + right + 1;
    }
}

110. 平衡二叉树

class Solution {
    public boolean isBalanced(TreeNode root) {
        int res = getHeight(root);
        return res == -1 ? false : true;
    }
    public int getHeight(TreeNode root){
        if(root == null) return 0;
        int leftDepth = getHeight(root.left); // 左
        if(leftDepth == -1) return -1;
        int rightDepth = getHeight(root.right); // 右
        if(rightDepth == -1) return -1;
        int res = 1;
        res = Math.abs(leftDepth - rightDepth) > 1 ? -1 : Math.max(leftDepth, rightDepth) + 1; // 中
        return res;
    }
}

257. 二叉树的所有路径

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new LinkedList<>();
        LinkedList<Integer> path = new LinkedList<>();
        if(root == null) return res;
        getPath(root, path, res);
        return res;
    }
    public void getPath(TreeNode root, LinkedList<Integer> path, List<String> res){
        path.offer(root.val);
        if(root.left == null && root.right == null){
            StringBuilder sb = new StringBuilder();
            for(Integer i : path){
                sb.append(i.toString());
                sb.append("->");
            }
            sb.delete(sb.length()-2, sb.length());
            res.add(sb.toString());
            return;
        }
        if(root.left != null){
            getPath(root.left, path, res);
            path.pollLast();
        }
        if(root.right != null){
            getPath(root.right, path, res);
            path.pollLast();
        }
    }
}

404. 左叶子之和

左叶子就是叶子结点,如果不是叶子节点则不算。使用递归法,后序遍历,在返回值上累加即可。

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if(root == null) return 0;
        
        int leftSum = sumOfLeftLeaves(root.left);
        if(root.left != null && root.left.left == null && root.left.right == null){
            leftSum += root.left.val;
        }
        int rightSum = sumOfLeftLeaves(root.right);
        return leftSum + rightSum;
    }
}

112. 路径总和

class Solution {
    public Boolean res = false;
    public boolean hasPathSum(TreeNode root, int targetSum) {
        findPath(root, 0, targetSum);
        return res;
    }
    public void findPath(TreeNode root, int sum, int target){
        if(root == null) return;
        sum += root.val; //中
        if(root.left == null && root.right == null && sum == target){
            res = true;
        }
        findPath(root.left, sum, target); //左
        findPath(root.right, sum, target); //右
    }
}

113. 路径总和 II

一道涉及回溯的问题,这里使用了前序遍历,因为要计算每个路径的和,每到一个节点都会加入了path列表中,然后遍历左节点和右节点;遍历完成后要删除path中的值

class Solution {
    public List<List<Integer>> res = new LinkedList<>();
    public List<Integer> path = new LinkedList<>();
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        findPath(root, 0, targetSum);
        return res;
    }
    public void findPath(TreeNode root, int sum, int target){
        if(root == null) return;
        path.add(root.val); //中
        sum += root.val; 
        if(root.left == null && root.right == null && sum == target){
            res.add(new ArrayList(path));
        }
        findPath(root.left, sum, target); // 左
        findPath(root.right, sum, target); // 右
        path.remove(path.size()-1); //回溯path, sum不需要回溯因为它是临时变量
    }
}

二叉树的构造

从中序遍历和后序遍历能还原唯一的二叉树,从中序遍历和前序遍历能还原唯一的二叉树。

主要思路就是后序遍历的最后一个值就是根节点,我们只需要从后序遍历入手,再找到这个值在中序遍历的索引(需要一个哈希表),就能把中序遍历分成3份,分变为[左子树,根节点,右子树],再递归创建右子树和左子树,就能创建出整个树。

值得注意的是,一定要先创建右子树,因为只能从右子树中得到根节点,再得到所有右子树的根节点后,左子树的索引才能确定。

从前序遍历构造也是同样的道理。

106. 从中序与后序遍历序列构造二叉树

class Solution {
    public HashMap<Integer, Integer> map = new HashMap<>();
    public int postIndex;
    public int[] inorder;
    public int[] postorder;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        this.inorder = inorder;
        this.postorder = postorder;
        postIndex = postorder.length - 1;
        int i = 0;
        for(int n: inorder){
            map.put(n, i++);
        }
        return build(0, inorder.length - 1);
    }
    public TreeNode build(int inLeft, int inRight){
        if(inLeft > inRight) return null;
        int val = postorder[postIndex--];
        TreeNode root = new TreeNode(val);

        int index = map.get(val);

        root.right = build(index + 1, inRight);
        root.left = build(inLeft, index - 1);
        return root;
    }
}

105. 从前序与中序遍历序列构造二叉树

class Solution {
    public HashMap<Integer, Integer> map = new HashMap<>();
    public int[] preorder;
    public int[] inorder;
    public int preIndex;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        this.inorder = inorder;
        preIndex = 0;
        int i = 0;
        for(int n : inorder){
            map.put(n, i++);
        }
        return build(0, preorder.length - 1);
    }
    public TreeNode build(int inLeft, int inRight){
        if(inLeft > inRight) return null;
        int val = preorder[preIndex++];
        TreeNode root = new TreeNode(val);
        int index = map.get(val);

        root.left = build(inLeft, index - 1);
        root.right = build(index + 1, inRight);

        return root;
    }
}

654. 最大二叉树

递归法求解,前序遍历,构造二叉树一般都是前序遍历,因为要先确定二叉树的根节点,再确定左右节点,时间复杂度O(n^2)

class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        if(nums.length == 0) return null;
        return build(nums, 0, nums.length - 1);
    }
    public TreeNode build(int[] nums, int left, int right){
        if(left > right) return null;
        int index = left;
        for(int i = left + 1; i <= right; i++){
            if(nums[i] > nums[index]) index = i;
        }
        TreeNode root = new TreeNode(nums[index]);

        root.left = build(nums, left, index - 1);
        root.right = build(nums, index + 1, right);
        return root;
    }
}

617. 合并二叉树

没什么说的,简单的递归,前序遍历

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if(root1 == null && root2 == null) return null;
        if(root1 == null && root2 != null) return root2;
        if(root1 != null && root2 == null) return root1;
        TreeNode root = new TreeNode(root1.val + root2.val);
        root.left = mergeTrees(root1.left, root2.left);
        root.right = mergeTrees(root1.right, root2.right);
        return root;
    }
}

二叉搜索树

二叉搜索树的主要特点就是有序,可以看成是一个有序的数组,这样求最值,最小差值都会很容易。

700. 二叉搜索树中的搜索

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        while(root != null){
            if(root.val == val) return root;
            if(root.val < val){
                root = root.right;
            }else{
                root = root.left;
            }
        }
        return null;
    }
}

98. 验证二叉搜索树

中序遍历递归法

class Solution {
    public TreeNode pre = null;
    public boolean isValidBST(TreeNode root) {
        if(root == null) return true;
        boolean left = isValidBST(root.left);
        if(pre != null && pre.val > root.val) return false;
        pre = root;
        boolean right = isValidBST(root.right);
        return left && right;
    }
}

中序遍历迭代法,使用null标记中节点。

class Solution {
    public boolean isValidBST(TreeNode root) {
        if(root == null) return true;
        TreeNode pre = null;
        Stack<TreeNode> s = new Stack<>();
        s.push(root);
        while(!s.isEmpty()){
            TreeNode node = s.peek();
            if(node != null){
                s.pop(); //弹出,避免重复操作
                if(node.right != null) s.push(node.right); // right
                s.push(node); //中
                s.push(null); //null标记中节点
                if(node.left != null) s.push(node.left); // left
            }else{
                s.pop(); //弹出空节点
                node = s.pop();
                if(pre != null && pre.val >= node.val) return false;
                pre = node;
            }
        }
        return true;
    }
}

530. 二叉搜索树的最小绝对差

中序遍历迭代法

class Solution {
    public int getMinimumDifference(TreeNode root) {
        Stack<TreeNode> s = new Stack<>();
        TreeNode pre = null;
        int diff = Integer.MAX_VALUE;
        s.push(root);
        while(!s.isEmpty()){
            TreeNode node = s.peek();
            if(node != null){
                s.pop();
                if(node.right != null) s.push(node.right);
                s.push(node);
                s.push(null);
                if(node.left != null) s.push(node.left);
            }else{
                s.pop();
                node = s.pop();
                if(pre != null) diff = Math.min(diff, Math.abs(node.val - pre.val));
                pre = node;
            }
        }
        return diff;
    }
}

中序遍历递归法

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

501. 二叉搜索树中的众数

一般来讲,求众数这种需要计算频率的问题,都会使用哈希表来记录频率,如下面这种算法。但这道题是二叉搜索树,一个有序的数组里,如果会出现众数,那他们一定是连续的,其实不需要一个哈希表,只需要记录前一个节点。

中序遍历,哈希表算法

class Solution {
    public LinkedList<Integer> res = new LinkedList<>();
    public Map<Integer, Integer> map = new HashMap<>();
    public int[] findMode(TreeNode root) {
        inorder(root);
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
    public void inorder(TreeNode root){
        if(root == null) return;
        inorder(root.left);
        map.put(root.val, map.getOrDefault(root.val, 0) + 1);
        if(res.size() != 0){
            if(map.get(root.val) > map.get(res.get(0))){
                LinkedList<Integer> result = new LinkedList<>();
                result.add(root.val);
                res = result;
            }
            else if(root.val != res.get(0) && map.get(root.val) == map.get(res.get(0))){
                res.add(root.val);
            }
        }else{
            res.add(root.val);
        }
        inorder(root.right);
    }
}

中序遍历,只记录前一个节点

class Solution {
    public LinkedList<Integer> res = new LinkedList<>();
    public TreeNode pre = null;
    public int count = 0;
    public int maxCount = 0;;
    public int[] findMode(TreeNode root) {
        inorder(root);
        return res.stream().mapToInt(Integer::valueOf).toArray();
    }
    public void inorder(TreeNode root){
        if(root == null) return;
        inorder(root.left);

        if(pre == null || root.val != pre.val){
            count = 1;
        }else{
            count++;
        }
        if(count > maxCount){
            res.clear();
            res.add(root.val);
            maxCount = count;
        }else if(count == maxCount){
            res.add(root.val);
        }
        pre = root;

        inorder(root.right);
    }
}

二叉树的共同祖先

236. 二叉树的最近公共祖先

最开始的思路肯定是要后序遍历,因为这道题要进行从下至上的遍历,只有后序遍历能从下至上查找。

那怎么才算找到了最近公共祖先呢?也就是说有哪些情况呢?

根据以上定义,若 root 是 p,q 的 最近公共祖先 ,则只可能为以下情况之一:

  • p 和 q 在 root 的子树中,且分列 root 的 异侧(即分别在左、右子树中)
  • p = root,且 q 在 root 的左或右子树中
  • q = root,且 p 在 root 的左或右子树中

这道题很难想出递归的终止条件,主要还是因为,一个节点自己也是自己的祖先。

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

235. 二叉搜索树的最近公共祖先

这道题的逻辑大致和[236]相同,区别是这道题是二叉搜索树,如下是二叉树找公共祖先的算法

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

二叉搜索树是有序的,那么公共祖先已一定在[p, q]区间或者[q, p]区间内,而在这个区间内的数是不是一定是最近的公共祖先呢?

是!所以我们不需要遍历整个树,找到这个节点就立刻返回,算法如下

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

701. 二叉搜索树中的插入操作

挺简单的题,不需要重构二叉搜索树,只要见缝插针就好

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if(root == null) return new TreeNode(val);
        TreeNode cur = root;
        traversal(cur, val);
        return root;
    }
    public void traversal(TreeNode root, int val){
        if(root == null) return;
        if(root.val < val) traversal(root.right, val);
        if(root.val > val) traversal(root.left, val);
        TreeNode node = new TreeNode(val);
        if(root.val < val && root.right == null) root.right = node;
        if(root.val > val && root.left == null) root.left = node;
        return;
    }
}

450. 删除二叉搜索树中的节点

在二叉搜索树中删除节点要比添加节点困难一些,主要是对树结构的修改,依然是遍历二叉搜索树(不用遍历全树),找到了要被删除的节点后,分为4种情况

  1. 找到了 为叶子结点
  2. 找到了 左子树为空
  3. 找到了 右子树为空
  4. 找到了 左右子树都不为空

复杂的是第4种,我们要将这个节点的左子树移植到这个节点的右子树的最左边,使整个树保持二叉搜索树的性质

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if(root == null) return root;
        if(root.val == key){
            // 1. 找到了 为叶子结点
            if(root.left == null && root.right == null){
                return null;
            }
            // 2. 找到了 左子树为空
            else if(root.left == null){
                return root.right;
            }
            // 3. 找到了 右子树为空
            else if(root.right == null){
                return root.left;
            }
            // 4. 找到了 左右子树都不为空
            else{
                TreeNode right = root.right;
                TreeNode left = root.left;
                while(right.left != null){
                    right = right.left;
                }
                right.left = left;
                return root.right;
            }
        }
        if(root.val > key) root.left = deleteNode(root.left, key);
        if(root.val < key) root.right = deleteNode(root.right, key);
        return root;
    }
}