Loading...

LeetCode分类专题(二)——二叉树1

  iwehdio的博客园:https://www.cnblogs.com/iwehdio/

学习自:

二叉树题目:

  • 二叉树:226、116、114、654、105、106、652
  • 二叉树序列化:297
  • 二叉搜索树:230、537、98、700、701、450

1、二叉树

  • 写树相关的算法,简单说就是,先搞清楚当前 root 节点该做什么,然后根据函数定义递归调用子节点,递归调用会让孩子节点做相同的事情。

翻转二叉树

image-20210116145306415

  • 通过观察,我们发现只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树。

    class Solution {
        public TreeNode invertTree(TreeNode root) {
            //递归基
            if(root==null) return null;
            TreeNode temp = root.left;
            root.left = root.right;
            root.right = temp;
            invertTree(root.left);
            invertTree(root.right);
            return root;
        }
    }
    
  • 值得一提的是,如果把交换左右子节点的代码放在后序遍历的位置也是可以的,但是放在中序遍历的位置是不行的。因为在中序遍历位置,两次访问的都是左节点的数据。

  • 二叉树题目的一个难点就是,如何把题目的要求细化成每个节点需要做的事情

填充二叉树节点的右侧指针

image-20210116145927009

image-20210116145935113

  • 如果只依赖一个节点的话,肯定是没办法连接「跨父节点」的两个相邻节点的。

  • 我们的做法就是增加函数参数,一个节点做不到,我们就给他安排两个节点,「将每一层二叉树节点连接起来」可以细化成「将每两个相邻节点都连接起来」。

    class Solution {
        public Node connect(Node root) {
            if(root==null) return null;
            connectTwoNode(root.left, root.right);
            return root;
        }
        public void connectTwoNode(Node left, Node right) {
            if(left==null) return;
            /**** 前序遍历位置 ****/
        	// 将传入的两个节点连接
            left.next = right;
            connectTwoNode(left.left, left.right);
            connectTwoNode(left.right, right.left);
            connectTwoNode(right.left, right.right);
        }
    }
    
  • 但是这样做,其实是有重复连接的。每个节点只需要做两次连接即可:

    image-20210116153304847

    • 将输入节点看作是根节点与null。绿框中的是左节点2的连接,红框是右节点3的连接。
    • 在这里connectTwoNode()的语义就是,连接left.left指向left.right,和连接left.right指向right.left(如果right不为null)
    class Solution {
    
        public Node connect(Node root) {
            if(root==null) return null;
            connectTwoNode(root, null);
            return root;
        }
        public void connectTwoNode(Node left, Node right) {
            if(left==null) return;
            /**** 前序遍历位置 ****/
        	// 将传入的两个节点连接
            left.next = right;
            connectTwoNode(left.left, left.right);
            connectTwoNode(left.right, left.next==null?null:left.next.left);
        }
    }
    

将二叉树展开为链表

image-20210116153715233

  • 这个函数的定义:

    • 给 flatten 函数输入一个节点 root,那么以 root 为根的二叉树就会被拉平为一条链表。
  • 流程:

    1、将 root 的左子树和右子树拉平。

    2、将 root 的右子树接到左子树下方,然后将整个左子树作为右子树。

    image-20210116154837403

    class Solution {
        public void flatten(TreeNode root) {
            if(root==null) return;
            flatten(root.left);
            flatten(root.right);
            TreeNode left = root.left;
            TreeNode right = root.right;
    
            root.right = left;
            //一定要将左子树置空
            root.left = null;
            //从根节点开始是为了考虑各种边界情况,因为root已判定不为空
            TreeNode p = root;
            while(p.right!=null){
                p = p.right;
            }
            p.right = right;
        }
    }
    

最大二叉树

image-20210116155130783

image-20210116155148542

image-20210116155156399

  • 对于构造二叉树的问题,根节点要做的就是把想办法把自己构造出来。

  • 遍历数组把找到最大值 maxVal,把根节点 root 做出来,然后对 maxVal 左边的数组和右边的数组进行递归调用,作为 root 的左右子树。

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

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

image-20210116160903322

  • 从前序遍历和中序遍历中找出根节点:就是前序遍历的第一个节点。

    image-20210116164052268

  • 只要按中序遍历序列中根节点的位置,就可以区分左右子树的节点个数,从而直接划分索引:

    image-20210116164203683

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return build(preorder, 0, preorder.length, inorder, 0, inorder.length);
    }
    
    public TreeNode build(int[] preorder, int plo, int phi, int[] inorder, int ilo, int ihi) {
        if(plo==phi) return null;
        //找出根节点的中序遍历中的位置
        int index = findRoot(preorder[plo], inorder, ilo, ihi);
        TreeNode root = new TreeNode(preorder[plo]);
        //左子树的长度
        int div = index - ilo;
        root.left = build(preorder, plo+1, plo+div+1, inorder, ilo, index);
        root.right = build(preorder, plo+div+1, phi, inorder, index+1, ihi);
        return root;
    }
    
    public int findRoot(int root, int[] order, int lo, int hi) {
        for(int i=lo; i<hi; i++) {
            if(root==order[i])  return i;
        }
        return lo;
    }
    }
    
  • 优化:使用HashMap,以节点的值为索引,键为值。这样可以直接获得索引位置,只需要初始化的时候遍历一次。

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

image-20210116165122438

  • 与中序和前序遍历完全类似:

    image-20210116170020334

    image-20210116170027219

    class Solution {
        public TreeNode buildTree(int[] inorder, int[] postorder) {
            Map<Integer, Integer> map = new HashMap<>();
            for(int i=0; i<inorder.length; i++) {
                map.put(inorder[i], i);
            }
    
            return build(inorder, 0, inorder.length, postorder, 0, postorder.length, map);
        }
    
        public TreeNode build(int[] inorder, int ilo, int ihi, int[] postorder, int plo, int phi, Map<Integer, Integer> map) {
            if(ilo==ihi) return null;
            int index = map.get(postorder[phi-1]);
            TreeNode root = new TreeNode(postorder[phi-1]);
            int div = index - ilo;
            root.left = build(inorder, ilo, index, postorder, plo, plo+div, map);
            root.right = build(inorder, index+1, ihi, postorder, plo+div, phi-1, map);
            return root;
        }
    }
    

寻找重复的子树

image-20210116170452975

image-20210116170501371

  • 如果你想知道以自己为根的子树是不是重复的,是否应该被加入结果列表中,你需要知道什么信息?

    1. 以我为根的这棵二叉树(子树)长啥样?
    2. 以其他节点为根的子树都长啥样?
  • 如何才能知道以自己为根的二叉树长啥样?

    • 其实看到这个问题,就可以判断本题要使用「后序遍历」框架来解决:
    void traverse(TreeNode root) {
        traverse(root.left);
        traverse(root.right);
        /* 解法代码的位置 */
    }
    
    • 我要知道以自己为根的子树长啥样,是得先知道我的左右子树长啥样,再加上自己,就构成了整棵子树的样子。
  • 怎么描述一棵二叉树的模样呢?

    • 二叉树的前序后序遍历结果(包括null的,比如用#指代null)可以描述二叉树的结构。
    • 扩展二叉树与其前序或后序序列是一一对应的
  • 怎么知道别人长啥样?

    • 借助一个外部数据结构,让每个节点把自己子树的序列化结果存进去。可以使用HashMap记录子树出现的次数。
  • 这里主要需要知道map.getOrDefault()方法:当Map集合中有这个key时,就使用这个key值,如果没有就使用默认值defaultValue.

    default V getOrDefault(Object key, V defaultValue) {
            V v;
            return (((v = get(key)) != null) || containsKey(key))
                ? v
                : defaultValue;
    }
    
class Solution {

    LinkedList<TreeNode> ans = new LinkedList<>();
    HashMap<String, Integer> map = new HashMap<>(); 
    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        traverse(root);
        return ans;
    }

    String traverse(TreeNode root) {
        if(root==null) return "#";
        String left = traverse(root.left);
        String right = traverse(root.right);
        String subTree = new StringBuilder(left).append(",").append(right).append(",").append(root.val).toString();
         int freq = map.getOrDefault(subTree, 0);
         if(freq==1) {
             ans.add(root);
         }
         map.put(subTree, freq+1);
         return subTree;
    }
}

2、序列化

二叉树的序列化与反序列化

image-20210117102609604

image-20210117102625008

  • 一棵二叉树能够被重建,如果满足下面三个条件之一:
      a1. 已知先序遍历;或
      a2. 已知后序遍历;或
      a3. 已知层序遍历;

  • 且满足下面三个条件之一:
      b1. 前面已知的那种遍历包含了空指针;或
      b2. 已知中序遍历,且树中不含重复元素;或
      b3. 树是二叉搜索树,且不含重复元素。

  • 前序遍历的序列化:

    image-20210117105716933

    • 递归函数的作用是,把自己加入到序列化字符串中。
    • 本质是,如果遇到null就返回。
  • 前序遍历反序列化:

    • 递归函数的作用是,把自己从序列化数组中取出。
    • 序列化数组中的第一个元素是根节点,本质是遇到null会返回到右子树。
public class Codec {
    String spl = ",";
    String nll = "#";
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        ser(root, sb);
        return sb.toString();
    }

    public void ser(TreeNode root, StringBuilder sb) {
        //递归基
        if(root==null) {
            sb.append(nll).append(spl);
            return;
        }
        sb.append(root.val).append(spl);
        ser(root.left, sb);
        ser(root.right, sb);
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        LinkedList<String> nodes = new LinkedList<>();
        for(String s : data.split(spl)) {
            nodes.add(s);
        }
        return deser(nodes);
    }

    public TreeNode deser(LinkedList<String> nodes) {
        //终止条件
        if(nodes.isEmpty()) return null;
        String first = nodes.removeFirst();
        //递归基
        if(first.equals(nll)) return null;
        TreeNode root = new TreeNode(Integer.parseInt(first));
        root.left = deser(nodes);
        //理解执行到这一步时,nodes数组中左子树的所有元素都已经被取出了,如果是后序遍历就要先right后left了
        root.right = deser(nodes);
        return root;
    }
}
  • 体会一下,在二叉树递归中返回值为节点类型,但是需要递归的修改传入参数的信息。

3、二叉搜索树

  • BST 的特性:

    1、对于 BST 的每一个节点 node,左子树节点的值都比 node 的值要小,右子树节点的值都比 node 的值大。

    2、对于 BST 的每一个节点 node,它的左侧子树和右侧子树都是 BST。

    3、BST 的中序遍历结果是有序的。

二叉搜索树中第K小的元素

image-20210117111552928

  • 中序遍历序列就是一个升序排列,所以只需要对其进行中序遍历,遍历到第k个元素时终止,并记录元素的值即可。

    class Solution {
        public int kthSmallest(TreeNode root, int k) {
            tranvrse(root, k);
            return ans;
        }
    
        int ans;
        int rank;
        public void tranvrse(TreeNode root, int k) {
            if(root==null) return;
    		
            tranvrse(root.left, k);
            
            //中序遍历位置,记录遍历到第几个了,从1开始
            rank++;
            if(k==rank) {
                ans = root.val;
                return;
            }
            
            tranvrse(root.right, k);
        }
    }
    
  • 优化:

    • 如果实现一个在二叉搜索树中通过排名计算对应元素的方法select(int k),每次都中序遍历太麻烦了。
    • 需要在节点中维护一个信息,就是该节点的值在这颗BST中排第几。
    • 这个信息具体可以是,以自己为根的这棵二叉树有多少个节点。有了size字段,外加 BST 节点左小右大的性质,对于每个节点node就可以通过node.left.size推导出node的排名。

把二叉搜索树转换为累加树

image-20210117113008954

image-20210117112953946

  • 每个节点都去计算右子树的和,是不行的。对于一个节点来说,确实右子树都是比它大的元素,但问题是它的父节点也可能是比它大的元素。

  • 利用 BST 的中序遍历特性,维护一个遍历到目前位置的累加变量:

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

验证二叉搜索树

image-20210117171741105

  • 仅仅只验证这个节点的值是否在其左右节点的值之间是不够的,因为应该是在左子树最右节点和右子树最左节点之间,否则可能出现:

    image-20210117171852984

  • 可以在参数中传入对该子树的大小限制,包括最大值和最小值。注意这里包括等于:

    class Solution {
        public boolean isValidBST(TreeNode root) {
            return valid(root, null, null);
        }
    
        public boolean valid(TreeNode root, Integer min, Integer max) {
            if(root==null) return true;
    
            if(min!=null && min>=root.val) return false;
            if(max!=null && max<=root.val) return false;
    
            return valid(root.left, min, root.val) && valid(root.right, root.val, max);
        }
    }
    

二叉搜索树中的搜索

image-20210117172207016

image-20210117172212359

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

二叉搜索树中的插入操作

image-20210117172820556

image-20210117172826540

  • 插入其实和搜索很像。虽然插入一个数据后的二叉搜索树可以有多种形态,但是把插入节点放在叶节点是最简单的,而且也不会破环二叉搜索树。

  • 具体来说,就是搜索这个值,在为null的节点处插入。相当于由于没有这个元素查找失败,但是这个元素在这个位置是合法的。

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

删除二叉搜索树中的节点

image-20210117173611956

image-20210117173619803

  • 进行删除的第一步是查找。如果找到了就删除。重点在于删除操作。

  • 删除可以按照找到的节点类型分为几种情况:

    1. 恰好是末端节点,两个子节点都为空,那么可以直接删除了。

      image-20210117173935139

    2. 只有一个非空子节点,那么它要让这个孩子接替自己的位置。

      image-20210117173957295

    3. 有两个子节点,麻烦了,为了不破坏 BST 的性质,必须找到左子树中最大的那个节点,或者右子树中最小的那个节点来接替自己。

  • 需要注意的是:

    • 一般不会通过 root.val = minNode.val 修改节点内部的值来交换节点,而是通过一系列略微复杂的链表操作交换 rootminNode 两个节点。
    • 对于情况三,交换节点的值后,还需要递归的删除左子树中最大的那个节点,或者右子树中最小的那个节点。
    class Solution {
        public TreeNode deleteNode(TreeNode root, int key) {
            if(root==null) return null;
            if(root.val==key) {
                if(root.left==null && root.right==null) 
                    return null;
                if(root.left==null) return root.right;
                if(root.right==null) return root.left;
                TreeNode max = findMax(root.left);
                root.val = max.val;
                root.left = deleteNode(root.left, max.val);
            }
            if(root.val > key) {
                root.left = deleteNode(root.left, key);
            } else {
                root.right = deleteNode(root.right, key);
            }
            
            return root;
        }
    
    
        public TreeNode findMax(TreeNode root) {
            if(root==null) return null;
            if(root.right==null) {
                return root;
            } else {
                return findMax(root.right);
            }
        }
    }
    

iwehdio的博客园:https://www.cnblogs.com/iwehdio/
posted @ 2021-01-17 18:20  iwehdio  阅读(126)  评论(0编辑  收藏  举报