文章目录
遇到二叉树相关的问题,多半和递归有关系,二叉树遍历也是递归遍历(从树得到序列),二叉树的构建也是递归构建(从序列得到一个二叉树)
依靠前序遍历数组和中序遍历数组可以唯一确定一棵二叉树;
依靠中序遍历数组和后序遍历数组可以唯一确定一棵二叉树;
但是,依靠前序遍历数组和后序遍历数组无法唯一确定一棵二叉树;
从前序与中序遍历序列构造二叉树
根据一棵树的前序遍历与中序遍历构造二叉树。当然也是力扣105的原题。
注意:你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
分析:
给定一个前序序列和一个中序序列,且里面没有重复的元素,如何构造一和二叉树呢?我们可以先单独观察两个序列的特征:
前序遍历:遍历规则为(根,[左侧区域],[右侧区域])。
中序遍历:遍历规则为([左侧区域],根,[右侧区域])。
其中前序遍历的左侧区域和中序遍历的左侧区域包含元素的范围相同,根也是相同的。所以可以进行这样的操作:
- 根据前序遍历的第一个找到根节点,可以确定根。
- 通过中序遍历找到根节点的值,这样可以知道左侧区域和右侧区域节点个数多少。
- 根节点左侧区域由前中序列确定的左侧区域确定,根节点的右侧节点由前中序序列的右侧区域确定。
具体的实现上,可以使用一个HashMap存储中序存储的序列,避免重复计算。实现的代码为:
/**
* Definition for a binary tree node.
* public class TreeNode { // leecode给你提供一个二叉树类给你用
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length==0)
return null;
TreeNode root=new TreeNode(preorder[0]);
Map<Integer, Integer>map=new HashMap<Integer, Integer>();
for(int i=0;i<inorder.length;i++)
{
// 为什么要把中序遍历的整个数组放到map中,
// 因为前序序列中确定的根节点要在中序序列中确定位置,
// 然后确定左右子树,所以需要将数组变为map,
map.put(inorder[i], i);
}
// 六个参数,preorder三个,inorder三个
return buildTree(preorder,0,preorder.length-1, map,0,inorder.length-1);
}
private TreeNode buildTree(int[] preorder, int preStart, int preEnd, Map<Integer, Integer> map, int inStart, int inEnd) {
if(preEnd<preStart||inEnd<inStart)
return null;
TreeNode node=new TreeNode(preorder[preStart]); // 拿着前序遍历数组的第一个元素,新建一个二叉树根结点,这个根节点是作为返回值的
int i=map.get(preorder[preStart]);// 每次前序序列确定的根节点在中序遍历中的位置i,然后分割左右子树
int leftlen=i-inStart;// i-inStart变为中序遍历左边的长度,即左子树长度
//六个参数,前序遍历三个,中序遍历三个,继续构造左子树
node.left=buildTree(preorder, preStart+1, preStart+leftlen, map, inStart, i-1);
//六个参数,前序遍历三个,中序遍历三个,继续构造右子树
node.right=buildTree(preorder, preStart+leftlen+1,preEnd, map, i+1, inEnd);
return node;
}
}
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
解释左子树:node.left=buildTree(preorder, preStart+1, preStart+1+leftlen, map, inStart, i-1);
之前是 return buildTree(preorder,0,preorder.length-1, map,0,inorder.length-1);
前序序列启始点:为什么之前是preStart=0,现在是preStart+1?因为去掉一个根节点,good
前序序列终点:为什么之前是preorder.length-1,现在是preStart+leftlen?因为之前是整个前序序列,现在是左子树,整个前序序列是preorder[0 到 preorder.length-1],左子树是preorder[preStart+1,(preStart+1)+(leftlen-1)]变为preorder[preStart+1,preStart+leftlen],leftlen是每次更新的,表示左子树长度,用中序序列算出来的,good
中序序列启始点:为什么之前是inStart=0,现在还是inStart?因为中序遍历的左子树本来就在左边,从最左边开始没问题,good
中序序列终点:为什么之前右子树终点是inorder.length-1,现在是i-1?因为根节点是i,所有中序遍历的左子树终点是i-1,good
返回值:为什么返回值node.left?因为子树的根节点node就是母树的左子树根节点node.left.
解释右子树:node.right=buildTree(preorder, preStart+leftlen+1,preEnd, map, i+1, inEnd);
之前是 return buildTree(preorder,0,preorder.length-1, map,0,inorder.length-1);
前序序列启始点:为什么之前是preStart=0,现在是preStart+leftlen+1?之前是整个树,现在是右子树,右子树根节点从 preStart+leftlen+1 开始。
前序序列终点:为什么之前是preorder.length-1,现在是preEnd(即还是preorder.length-1)?对于前序
序列(中左右)来说,根节点在最前面,整个树的结束和右子树的结束是一样的。
中序序列启始点:为什么之前是inStart=0,现在还是i+1?因为之前的整个树,所以中序遍历从下标0开始,现在是右子树,刚才算出根节点在中序序列的下标识i,所以右子树中,中序序列从(i+1)开始。
中序序列终点:为什么之前右子树终点是inorder.length-1,现在是inEnd(即还是inorder.length-1)?对于中序序列(左中右)来说,根节点在中间,整个树的结束和右子树的结束是一样的。
返回值:为什么返回值node.right?因为对于右子树来说,子树的根节点node就是母树的左子树根节点node.right.
关于中序序列从数组变为map,为什么新得到的map,key存放数组元素,value存放数组下标?
这是由map这个数据结构从查找性质决定的,在map中,根据key可以找到唯一确定的value,但是通过value无法找到唯一确定的key,每次通过前序序列确定根节点后,要通过中序序列确定根节点的位置,所有只能下标放value,元素放在key。
为什么buildTree()方法最后返回来一个node?
这个node是整个树的根节点,将整个树构建完成后返回这个根节点,为什么呢?因为只有从这个根节点,就可以遍历整颗树(无论是前序遍历、中序遍历还是后序遍历),所以返回了整个树的根节点就相当于返回了整个二叉树。
从中序与后序遍历序列构造二叉树
根据一棵树的中序遍历与后序遍历构造二叉树,力扣106题
注意:你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
分析:
有了上面的分析,那么通过一个后序遍历和中序遍历去构造一棵二叉树,其实原理和前面的也是一样的。
中序遍历:遍历规则为([左侧区域],根,[右侧区域])。
后序遍历:遍历规则为([左侧区域],[右侧区域],根)。
具体实现的代码为:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode buildTree(int[] inorder,int[] postorder) {
if(postorder.length==0)
return null;
Map<Integer, Integer>map=new HashMap<Integer, Integer>();
for(int i=0;i<inorder.length;i++)
{
map.put(inorder[i], i); // 中序序列从数组变为map
}
//六个参数,前三个参数是后序序列,后三个参数是中序序列
return buildTree(postorder,0,postorder.length-1, map,0,inorder.length-1);
}
private TreeNode buildTree(int[] postorder, int postStart, int postEnd, Map<Integer, Integer> map, int inStart, int inEnd) {
if(postEnd<postStart||inEnd<inStart)
return null;
TreeNode node=new TreeNode(postorder[postEnd]);
int i=map.get(postorder[postEnd]);
int leftlen=i-inStart;
node.left=buildTree(postorder, postStart,postStart+leftlen-1, map, inStart, i-1);
node.right=buildTree(postorder, postStart+leftlen,postEnd-1, map, i+1, inEnd); return node;
}
}
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
如果理解了leetcode105,这个就很容易理解了,没必要写了,还是写一下吧
解释左子树:node.left=buildTree(postorder, postStart,postStart+leftlen-1, map, inStart, i-1);
之前是 buildTree(postorder,0,postorder.length-1, map,0,inorder.length-1);
后序序列启始点:为什么之前是preStart=0,现在还是0?因为后序序列(左右中),母树的最左节点也就是左子树的最左节点。
后序序列终点:为什么之前是preorder.length-1,现在是preStart+leftlen-1?之前是母树是后序序列,现在是母树的左子树的后序序列,对于母树的左子树的后序序列,由于长度为leftlen,所以最后一个节点就是 preStart+leftlen-1 。
中序序列启始点:为什么之前是inStart=0,现在还是inStart?对于中序序列(左中右),母树的最左节点也就是左子树的最左节点。
中序序列终点:为什么之前右子树终点是inorder.length-1,现在是i-1?对于中序序列(左中右),由于确定了根节点所在下标是i,所有左子树最后节点就是i-1。
返回值:为什么返回值node.left?因为子树的根节点node就是母树的左子树根节点node.left.
解释右子树:node.right=buildTree(postorder, postStart+leftlen,postEnd-1, map, i+1, inEnd);
之前是 buildTree(postorder,0,postorder.length-1, map,0,inorder.length-1);
后序序列启始点:为什么之前是preStart=0,现在是preStart+leftlen?现在是右子树,对于后序序列来说(左右中),右子树的开始节点就是,左子树开始节点postStart + 左子树长度leftlen,所以就是preStart+leftlen。
后序序列终点:为什么之前是preorder.length-1,现在是preEnd-1(即还是preorder.length-2)?现在是遍历右子树,对于后序序列来说,最后一个是母树的根节点,所有要去掉,所以就是preEnd-1了。
中序序列启始点:为什么之前是inStart=0,现在还是i+1?对于中序序列(左中右),由于确定了根节点所在下标是i,所以右子树最后节点就是i+1。
中序序列终点:为什么之前右子树终点是inorder.length-1,现在是inEnd(即还是inorder.length-1)?对于中序序列(左中右),母树最右端也就是右子树最右端,所以是一样的。
返回值:为什么返回值node.right?因为对于右子树来说,子树的根节点node就是母树的左子树根节点node.right.