LeetCode刷题—重建二叉树

LeetCode中有三道很类似的题,思路也大同小异,故作一总结。

105,从前序与中序遍历序列构造二叉树,medium 106,根据中序和后序遍历构造二叉树,medium 889,根据前序和后序遍历构造二叉树,medium

这三道题都是给出两种遍历方式,由其遍历顺序可发现规律来重建此二叉树。下面详解105题,后两题思路稍作改变,比较好懂了。

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

根据一棵树的前序遍历与中序遍历构造二叉树。

注意: 你可以假设树中没有重复的元素。

例如,给出

前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7
方法一:分治思想 + HashMap
  • 前序遍历:根-左-右。

    中序遍历:左-根-右。

    以题目示例为例: Picture1.png

    • 前序遍历划分 [ 3 | 9 | 20 15 7 ]
    • 中序遍历划分 [ 9 | 3 | 15 20 7 ]

    则前序遍历的首个元素 为 根节点的值,在中序遍历数组中找到 根节点所在索引,其左边的元素为 根节点的左子树右边的元素为 根节点的右子树

  • 以上子树的递推性质是 分治算法 的体现,考虑通过递归对所有子树进行划分。

    辅助函数 build,参数为:前序遍历的起点索引 preL,结束索引 preR,中序遍历的起点索引 inL,结束索引 inR。

    通过上面所述找到 root 在中序遍历的索引,递归其左边元素和 右边元素,分别赋给 root 的左子树 和 右子树。递归出口为 preL > preR || inL > inR,表示越过叶子节点,此时返回null。

    • 其中 找到 root 在中序遍历的索引 一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用 HashMap 快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的索引。

    • 确定 preL、preR、inL、inR 需要解方程 在这里插入图片描述

      左子树 在 前序遍历中的起点索引:preL + 1

      左子树 在 前序遍历中的结束索引:x

       x 满足 x - (preL + 1) = rootIndex - 1 - intL=>  x = rootIndex - intL + preL

      左子树 在 中序遍历中的起点索引:intL

      左子树 在 中序遍历中的结束索引:rootIndex - 1

      右子树 在前序遍历中的起点索引:x + 1

      右子树 在 前序遍历中的结束索引:preR

      右子树 在 中序遍历中的起点索引:rootIndex + 1

      右子树 在 中序遍历中的结束索引:inR

  • 代码:
    class Solution {
    private Map<Integer, Integer> map;
    private int[] preorder;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
    this.preorder = preorder;
    map = new HashMap<>();
    //将中序遍历数组的元素和其索引存入map键值对
    for(int i = 0; i < inorder.length; i++){
    map.put(inorder[i], i);
    }
    return build(0, preorder.length - 1, 0, inorder.length - 1);
    }
    //前序遍历:preL起始索引,preR结束索引;中序遍历:inL起始索引,inR结束索引
    public TreeNode build(int preL, int preR, int inL, int inR){
    if(preL > preR || inL > inR) return null;
    //将前序遍历首个元素设为根节点
    int rootVal = preorder[preL];
    TreeNode root = new TreeNode(rootVal);
    //找到中序遍历中root的索引
    int rootIndex = map.get(rootVal);
    //递归调用中序遍历root左右部分形成root的左右子树
    root.left = build(preL + 1, rootIndex - inL + preL, inL, rootIndex - 1);
    root.right = build(rootIndex - inL + preL + 1, preR, rootIndex + 1, inR);
    return root;
    }
    }
    ##### 方法二
  • 和上面总体思想一样,但不借助 HashMap 来定位 root 在中序遍历数组的索引,而是用 List 的 indexOf 来定位,用 subList 确定左右子树的范围。

  • 代码:

    class Solution {

    public TreeNode buildTree(int[] preorder, int[] inorder) {
    List<Integer> prelist = new ArrayList<>();
    List<Integer> inlist = new ArrayList<>();
    //将int数组转为List
    for(int i = 0; i < preorder.length; i++){
    prelist.add(preorder[i]);
    inlist.add(inorder[i]);
    }
    return build(prelist, inlist);
    }
    public TreeNode build(List<Integer> prelist, List<Integer> inlist){
    if(inlist.isEmpty()) return null;
    //前序集合的首个元素为根节点
    int rootVal = prelist.remove(0);
    TreeNode root = new TreeNode(rootVal);
    //找到根节点在中序的索引
    int index = inlist.indexOf(rootVal);
    //新的左右两边
    root.left = build(prelist, inlist.subList(0, index));
    root.right = build(prelist, inlist.subList(index + 1, inlist.size()));

    return root;
    }
    }
    先把数组转化为list集合,然后在list集合中进行截取,这样效率明显不是很高。 #### 106,根据中序和后序遍历构造二叉树,medium 根据一棵树的中序遍历与后序遍历构造二叉树。

注意: 你可以假设树中没有重复的元素。

例如,给出

中序遍历 inorder = [9,3,15,20,7] 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树:

    3
   / \
  9  20
    /  \
   15   7
  • 思路:和上一题的思路基本一致,由中序遍历的规律(root 左部分为左子树,右部分为右子树),后序遍历的根节点为最后的节点。

  • 代码:

    class Solution {
    private Map<Integer, Integer> map;
    private int[] postorder;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
    this.postorder = postorder;
    map = new HashMap<>();
    for(int i = 0; i < inorder.length; i++){
    map.put(inorder[i], i);
    }
    return build(0, inorder.length - 1, 0, postorder.length - 1);
    }
    public TreeNode build(int inL, int inR, int postL, int postR){
    if(inL > inR || postL > postR) return null;
    //在后序数组中找到根节点root
    int rootVal = postorder[postR];
    TreeNode root = new TreeNode(rootVal);
    //中序中root的索引
    int rootIndex = map.get(rootVal);
    root.left = build(inL, rootIndex - 1, postL, postL + rootIndex - 1 - inL);
    root.right = build(rootIndex + 1, inR, postL + rootIndex - inL, postR - 1);
    return root;
    }
    }
    #### 889,根据前序和后序遍历构造二叉树,medium 返回与给定的前序和后序遍历匹配的任何二叉树。

pre 和 post 遍历中的值是不同的正整数。

示例:

输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1] 输出:[1,2,3,4,5,6,7]

  • 思路:还是按照之前的思路,找到根节点(即前序遍历的第一个元素或后序遍历的最后一个元素),但前序和后序 不像 中序的根作为分隔点,所以需要再找左子树的根 在 后序遍历数组 的 索引,依次来确定左右子树的长度。

  • 代码:
    class Solution {
    Map<Integer,Integer> map;
    private int[] post;
    private int[] pre;
    public TreeNode constructFromPrePost(int[] pre, int[] post) {
    this.post = post;
    this.pre = pre;
    map = new HashMap<>();
    for(int i = 0; i < post.length; i++){
    map.put(post[i], i);
    }
    return build(0, pre.length - 1, 0, post.length - 1);
    }
    public TreeNode build(int preL, int preR, int postL, int postR){
    if(preL > preR || postL > postR) return null;
    //不加这步,下面int leftRoot = pre[preL + 1];会出现角标越界
    if(preL == preR) return new TreeNode(pre[preL]);

    //根的索引即前序第一个元素和后序最后一个元素
    TreeNode root = new TreeNode(post[postR]);
    //下面是为了确定左子树在数组中的长度
    //左子树根节点
    int leftRoot = pre[preL + 1];
    //左子树根节点在后序的索引
    int leftRootIndex = map.get(leftRoot);

    root.left = build(preL + 1, leftRootIndex - postL + preL + 1, postL, leftRootIndex);
    root.right = build(leftRootIndex - postL + preL + 2, preR, leftRootIndex + 1, postR - 1);
    return root;
    }
    }
    #### 总结
  • 三个题中 前序 + 中序 和 后序 + 中序,都利用中序的根左边部分是左子树,右边部分是右子树。所以先建立根节点,并找出根节点在中序遍历数组的索引,就可以利用此规律重建二叉树。
  • 只有 前序 + 后序时,无法利用中序遍历的规律,则需要再找出左子树的根节点,来确定左、右子树的长度。
  • 用 HashMap 来以空间换时间,快速定位。
  • 注意三题都不能有重复数字,才能用上面方法。

------ 本文结束感谢您的阅读 ------