zoukankan      html  css  js  c++  java
  • 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/
    来源与结束于否定之否定。
  • 相关阅读:
    管理经济学
    第五章 文件管理
    内存管理
    第三章 进程调度与死锁
    操作系统概论 第二章
    计算机系统
    企业与政府信息资源管理
    信息资源管理的标准与法规
    议论文
    如何从大量的 url 中找出相同的 url
  • 原文地址:https://www.cnblogs.com/iwehdio/p/14289717.html
Copyright © 2011-2022 走看看