树的构建

二叉树的构建(前中、前后、中后)

关联LeetCode105,106,889

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

LeetCode 106根据一棵树的中序遍历与后序遍历构造二叉树

LeetCode 889根据前序和后序遍历构造二叉树

这三题建议一起做,可以很好地熟悉递归以及二叉树

所需要的基础知识(熟悉的可以跳过):

树的三种遍历
  • 前序遍历(先访问根结点,再访问左子树,最后访问右子树,简称根左右)
  • 中序遍历(左根右)
  • 后序遍历(左右根)

下面给出对应的java代码

  1. 前序遍历
//前序遍历
public void preOrderRecur(TreeNode root){
    if(root==null){
       return;
    }
    //前序遍历:根->左->右
    System.out.println(root.value+" ");//打印相当于访问根结点
    preOrderRecur(root.left);
    preOrderRecur(root.right);
}
  1. 中序遍历
//中序遍历:左->根->右
public void inOrderRecur(TreeNode root){
    	if (root==null){
         return;
      }
      inOrderRecur(root.left);
      System.out.println(root.value+" ");
      inOrderRecur(root.right);
}
  1. 后序遍历
//后序遍历:左->右->根
public void posOrderRecur(TreeNode root){
    if(root==null){
       return;
    }
    posOrderRecur(root.left);
    posOrderRecur(root.right);
    System.out.println(root.value+" ");
}

**补充:**这三种遍历方式也可以看作三种递归框架,只需要按照上述遍历的顺序,将遍历函数换成需要递归的函数,输出函数换成对根结点的处理.看之后的解法进行比较,加深理解.

以前序为例,如果是在你的递归算法中需要先对树的根结点先进行处理,那么就采用前序遍历框架

即如下的顺序:

  1. 先写==base case,==递归到什么情况下不再深入开始返回(如上面,一般是根结点为空)
  2. 先对根结点root进行处理
  3. 调用函数对root的左子树进行处理
  4. 调用函数对root的右子树进行处理
  5. 返回根结点root

下面给出这三道题的递归解法

写二叉树的算法题,都是基于递归框架的,我们先要搞清楚 root 节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。源自labuladong

对于这三个题目而言,需要采用前序的递归框架

因为构建树都是先构建根结点root->构建root的左子树->构建root的右子树.

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

例如,给出

前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]

构建二叉树

在这里插入图片描述

根据前序遍历和中序遍历的特点,我们可以得到亮点结论

  1. 3是根结点(前序遍历中第一个访问根结点)
  2. 9是根结点3的左子树上的结点,[15,20,7]是根节点点右子树上的结点

在这里插入图片描述

也就是说

  1. 我们根据前序遍历数组preOrder中的第一个值就是root的值

  2. 然后再在中序遍历中定位到根结点(具体实现的过程中用到了hashMap)

  3. 然后在中序遍历中找到左子树和右子树的索引范围.同时也能够对左右子树的长度进行确定

  4. 由于是同一个树,无论对其进行什么遍历,它的左子树长度都是固定的,因此可以在前序遍历中确定左右子树的索引范围

  5. 对root的结点的左子树,传入左子树的前序遍历数组和中序遍历数组,创建root的左子树==(递归)==

  6. 对root的结点的右子树,传入右子树的前序遍历数组和中序遍历数组,创建root的右子树==(递归)==

  7. 返回root结点

给出伪代码:

buildMyTree(preOrder,pLeft,pRight,inOrder,iLeft,iRight){
		//preOrder前序遍历,inOrder中序遍历
		//前序遍历的左边界pLeft,右边界pRight
		//中序遍历的左边界iLeft,右边界iRight
		根据前序遍历找到根结点的值root_val,根据root_val构建根结点root;
		根据root_val在中序遍历中找到它的位置index;
		//root的左子树:
		//它的中序遍历:[iLeft,index-1],长度size_ltree=index-1-iLeft+1=index-iLeft;
		//它的前序遍历:[pLeft+1,pLeft+size_ltree]
		//root的右子树
		//它的中序遍历:[index+1,iRight]
		//它的前序遍历:[pLeft+size_ltree+1,pRight]
		root.left=buildMyTree(preOrder,pLeft+1,pLeft+size_ltree,inOrder,iLeft,index-1)
		
}

完整的java代码如下

可以看到核心是递归函数buildMyTree和上述伪代码基本一致,范围的计算细心点就不会错.

    private Map<Integer,Integer> indexMap;
    public TreeNode buildTree(int[] preOrder,int[] inOrder){
        int n=preOrder.length;
        indexMap=new HashMap<Integer,Integer>();
        for (int i = 0; i < n; i++) {
            indexMap.put(inOrder[i],i);
        }

        return buildMyTree(preOrder,0, n-1,inOrder,0,n-1 );
    }

    public TreeNode buildMyTree
            (int[] preOrder,int pLeft,int pright,int[] inOrder,int iLeft,int iRight){
        //base case
        //左边界不会大于右边界
        if(pLeft>pright){
            return null;
        }
        //找到根结点的位置
        TreeNode root = new TreeNode(preOrder[pLeft]);
        int index = indexMap.get(preOrder[pLeft]);
        //左子树中的结点数目
        int size_ltree=index-iLeft;
        root.left= buildMyTree(preOrder,pLeft+1,pLeft+size_ltree,inOrder,iLeft,index-1);

        root.right= buildMyTree(preOrder,pLeft+size_ltree+1,pright,inOrder,index+1,iRight);

        return  root;

    }

下面的这段代码是创建一个键值对,其中key是结点的值,value是对应在中序遍历的位置:

indexMap=new HashMap<Integer,Integer>();
for (int i = 0; i < n; i++) {
   indexMap.put(inOrder[i],i);
}
LeetCode106从中序与后序遍历序列构造二叉树

其实后序遍历和前序遍历的效果是一样的,就是用来确定根结点,只不过在前序遍历的数组中根结点的值是第一个,而在后序遍历的数组中根结点的值是最后一个.(一首一尾)

类比LeetCode105,可以写出如下代码:

    private Map<Integer,Integer> indexMap;
    public TreeNode buildTree(int[] inOrder,int[] postOrder){
        int n=postOrder.length;
        indexMap=new HashMap<Integer,Integer>();
        for (int i = 0; i < n; i++) {
            indexMap.put(inOrder[i],i);
        }

        return buildMyTree(postOrder,0, n-1,inOrder,0,n-1 );
    }

    private TreeNode buildMyTree
      (int[] postOrder, int pleft, int pright, int[] inOrder, int ileft, int iright) {
        if(ileft>iright ||pleft>pright){
            return null;
        }
        int rootval=postOrder[pright];
        TreeNode root = new TreeNode(rootval);
        int index=indexMap.get(rootval);//找到中序遍历中根的位置

        root.left=buildMyTree(postOrder,pleft,pleft+index-ileft-1,inOrder,ileft,index-1);
        root.right=buildMyTree(postOrder,pright-iright+index,pright-1,inOrder,index+1,iright);
        return  root;
    }
LeetCode 889根据前序和后序遍历构造二叉树

这一题和前两题最大的不同在于没有中序遍历,不能根据根结点很轻松地划分出左子树和右子树

前序遍历:根->左->右

后序遍历:左->右->根

关键还是找根

结合例子和图说

前序遍历pre = [1,2,4,5,3,6,7]
后序遍历post = [4,5,2,6,7,3,1]

在这里插入图片描述

1.如上图所示,先在前序遍历中找到左子树的根结点2

2.再在后序遍历中找到2的位置,这样就能确定左子树的结点范围和长度,进而也能确定后序遍历右子树的索引范围

3.根据左右子树的长度确定前序遍历中左右子树的索引范围

在编写代码之前需要注意一点

除了之前的base case之外,如果左子树或者右子树的结点的个数只有一个时,直接返回root.

那为啥之前两道题,因为之前两道题没有这个判断呢,因为之前的两题中有中序遍历,在找到根节点root后,如果它没有左子树或者右子树时,那么它的左边界(root的位置)会大于右边界,因为右边界=root-1能够包含在之前的base case里面,而这里却不行.需要单独拿出来.

public TreeNode constructFromPrePost(int[] pre, int[] post) {
        return buildMytree(pre,0,pre.length,post,0,post.length);
    }

    public TreeNode 
      buildMytree(int[] pre,int preLeft,int preRight,int[] post,int postLeft,int postRight){
        if(preLeft>preRight||postLeft>postRight){
            return null;
        }
        //首先对根结点进行处理
        TreeNode root = new TreeNode(pre[preLeft]);
        //下面这一行非常重要
        if(preLeft==preRight){
            return root;
        }
        int index=0;
        int leftSubtree_root=pre[preLeft+1];//左子树的根的值
        for (int i = postLeft; i <postRight ; i++) {
            if(leftSubtree_root==post[i]){
                index=i;
                break;
            }
        }
        int offset=index-postLeft;//leftSubtree_
        root.left=buildMytree(pre,preLeft+1,preLeft+1+offset,post,postLeft,index);
        root.right=buildMytree(pre,preLeft+2+offset,preRight,post,index+1,postRight-1);
        return  root;
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值