二叉树的构建(前中、前后、中后)
关联LeetCode105,106,889
LeetCode 105根据一棵树的前序遍历与中序遍历构造二叉树
LeetCode 106根据一棵树的中序遍历与后序遍历构造二叉树
LeetCode 889根据前序和后序遍历构造二叉树
这三题建议一起做,可以很好地熟悉递归以及二叉树
所需要的基础知识(熟悉的可以跳过):
树的三种遍历
- 前序遍历(先访问根结点,再访问左子树,最后访问右子树,简称根左右)
- 中序遍历(左根右)
- 后序遍历(左右根)
下面给出对应的java代码
- 前序遍历
//前序遍历
public void preOrderRecur(TreeNode root){
if(root==null){
return;
}
//前序遍历:根->左->右
System.out.println(root.value+" ");//打印相当于访问根结点
preOrderRecur(root.left);
preOrderRecur(root.right);
}
- 中序遍历
//中序遍历:左->根->右
public void inOrderRecur(TreeNode root){
if (root==null){
return;
}
inOrderRecur(root.left);
System.out.println(root.value+" ");
inOrderRecur(root.right);
}
- 后序遍历
//后序遍历:左->右->根
public void posOrderRecur(TreeNode root){
if(root==null){
return;
}
posOrderRecur(root.left);
posOrderRecur(root.right);
System.out.println(root.value+" ");
}
**补充:**这三种遍历方式也可以看作三种递归框架,只需要按照上述遍历的顺序,将遍历函数换成需要递归的函数,输出函数换成对根结点的处理.看之后的解法进行比较,加深理解.
以前序为例,如果是在你的递归算法中需要先对树的根结点先进行处理,那么就采用前序遍历框架
即如下的顺序:
- 先写==base case,==递归到什么情况下不再深入开始返回(如上面,一般是根结点为空)
- 先对根结点root进行处理
- 调用函数对root的左子树进行处理
- 调用函数对root的右子树进行处理
- 返回根结点root
下面给出这三道题的递归解法
写二叉树的算法题,都是基于递归框架的,我们先要搞清楚
root
节点它自己要做什么,然后根据题目要求选择使用前序,中序,后续的递归框架。源自labuladong
对于这三个题目而言,需要采用前序的递归框架
因为构建树都是先构建根结点root->构建root的左子树->构建root的右子树.
LeetCode 105 从前序与中序遍历序列构造二叉树
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
构建二叉树
根据前序遍历和中序遍历的特点,我们可以得到亮点结论
- 3是根结点(前序遍历中第一个访问根结点)
- 9是根结点3的左子树上的结点,[15,20,7]是根节点点右子树上的结点
也就是说
-
我们根据前序遍历数组preOrder中的第一个值就是root的值
-
然后再在中序遍历中定位到根结点(具体实现的过程中用到了hashMap)
-
然后在中序遍历中找到左子树和右子树的索引范围.同时也能够对左右子树的长度进行确定
-
由于是同一个树,无论对其进行什么遍历,它的左子树长度都是固定的,因此可以在前序遍历中确定左右子树的索引范围
-
对root的结点的左子树,传入左子树的前序遍历数组和中序遍历数组,创建root的左子树==(递归)==
-
对root的结点的右子树,传入右子树的前序遍历数组和中序遍历数组,创建root的右子树==(递归)==
-
返回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;
}