题目
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
解析
预备知识
要想做好这样的题目,首先应该深刻理解了树的遍历形式。树有4种遍历形式。
1. 前序遍历:先访问根节点,然后访问左子节点,最后访问右子节点。简记为“中-左-右”
2. 中序遍历:先访问左子节点,然后访问根节点,最后访问右子节点。简记为“左-中-右”
3. 后序遍历,先访问左子节点,然后访问右子节点,最后访问根节点。简记为“左-右-中”
4. 层序遍历。先访问数的第一层节点,然后访问第二层,直到最后一层。简记为按层级顺序一层一层的访问。
思路一
综合上面的介绍树的遍历方式,我们发现前序遍历的第一个节点必然是这棵树的根节点。而在中序遍历中,由于其特殊的访问顺序规则根节点总是在中间,处于根节点左边则是左子树的节点集合,而处于根节点右边则是右子树的节点集合。所以我们根据根节点找出左右子树后,又可以对左右子树按照同样的思路来处理。因此此处采用递归非常合适。
下面我们结合例子,简单复述一下上面的处理过程。
前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}。
1. 根据前序遍历的第一个节点,我们可以知道这课树的根节点为1.
2. 然后我们在中序遍历中找到我们的根节点1所处的位置,position = 3
3. 我们得出根节点1的左子树节点集合为{4,7,2}。右子树的节点集合为{5,3,8,6}
4. 接下来我们处理左子树,同样根据前序遍历可知左子树集合4,7,2的根节点为2,因为树的前序遍历的第一个节点必为根节点呀。
5. 然后我们在中序遍历中找到我们的根节点2所处的位置,position = 2
6. 我们得出根节点2的左子树节点集合为{4,7}。右子树的节点集合为为空,说明该节点没有右子树。
7. 之后我们继续按如下的操作的处理左子树集合{4,7}
……..
按照以上步骤,最终就会重构出我们的二叉树。这里可以发现只要子树的区间大小变为0了,说明父节点没有对应的子树,因此就不必继续递归处理,而是开始回溯。还有一点,我们是按照先左后右的递归处理,所以前序遍历中的每次都是按顺序去取来作为根节点的。所以可以用一个全局的index来控制前序的访问位置即可。我们只需关注中序遍历中每次划分的区间大小即可。
//标识前序数组中位置
static int index;
public static TreeNode reConstructBinaryTree1(int [] pre,int [] in) {
index = 0;
int length = pre.length;
return reConstructBinaryTree(pre, in, 0, length - 1);
}
/**
* 递归版实现重建二叉树
* @param pre
* @param in
* @param s 重建区间的开始位置,inclusive
* @param e 重建区间的结束位置,inclusive
* @return
*/
private static TreeNode reConstructBinaryTree(int[] pre, int[] in, int s, int e) {
TreeNode treeNode = null;
if(s <= e) {
treeNode = new TreeNode(pre[index]);
//看看当前的子树的根节点在中序的哪个位置
int i = getiByVal(pre[index++], in, s, e);
//递归子树根节点的左子树
treeNode.left = reConstructBinaryTree(pre, in, s, i - 1);
//递归子树根节点的右子树
treeNode.right = reConstructBinaryTree(pre, in, i + 1, e);
}
return treeNode;
}
/**
* 获得根节点在中序遍历的位置
* 复杂度为线性,当然你可以利用哈希
* @param target
* @param in
* @return
*/
private static int getiByVal(int target, int[] in, int s, int e) {
for(int i = s; i <= e; i++) {
if(target == in[i]) {
return i;
}
}
return -1;
}
思路二
我们也可以不必用全局索引去记录前序的访问顺序,而是直接用数组来存放待处理的左右子树。我们根据中序遍历中根节点的位置,直接划分出左右子树,然后用相应的数组存放即可。
根据在前序遍历的第一个节点,可以得到根节点,然后根据根节点可以得到左右子树的中序遍历。由左右子树的大小即可从总的前序遍历中得到该左右子树的前序遍历。在前序遍历中,从根节点往后数左子树的大小个即可得到左子树的前序遍历,在往后数右子树的大小个即可得到右子树的前序遍历。
这样的我们的问题已知左右子树的前序中序遍历,重建二叉树。递归即可。
好处:
1. 就是可以直接取子树序列的第一个节点作为当前子树的根节点
2. 我们每次都可以获得当前的前序序列和中序序列,代码简单易懂,思路明确。
坏处:
1. 需要额外的存储空间来存放子树序列
2. 大量的数组复制操作
/**
* 改良做法
* @param pre
* @param in
* @return
*/
public static TreeNode reConstructBinaryTree2(int [] pre,int [] in) {
if(pre.length == 0 || in.length == 0) {
return null;
}
TreeNode treeNode = new TreeNode(pre[0]);
for(int i = 0; i < in.length; i++) {
if(pre[0] == in[i]) {
treeNode.left = reConstructBinaryTree2(Arrays.copyOfRange(pre, 1, i + 1),
Arrays.copyOfRange(in, 0, i));
treeNode.right = reConstructBinaryTree2(Arrays.copyOfRange(pre, i + 1, pre.length),
Arrays.copyOfRange(in, i + 1, in.length));
break;
}
}
return treeNode;
}