题目
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。
重建后的二叉树如下图所示:
二叉树的定义如下:
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
解题分析
首先,想解这道题,我认为首先要确认的是,你知道二叉树的前、中、后序遍历么?
所谓二叉树的前中后序锁参照的是,什么时候输出当前节点,就比如说前序遍历,他是先输出当前节点,然后在依次输出左子树和右子树;中序遍历就是输出左子树,然后输出当前节点,最后输出右子树;当然,后续遍历就是先输出左右子树,再打印当前节点;
以前序遍历为例来说一下题目所示的二叉树为什么前序遍历的结果为{1,2,4,7,3,5,6,8},因为,依据前序遍历的规定,我们先拿到根节点1,就要先打印出来,所以,先输出1,然后递归到它的左节点2,直接输出,然后再递归到2的左节点输出4,节点4没有左节点,然后递归节点4的右节点,输出节点7,然后返回到节点2的右节点,节点2没有右节点,于是返回到节点1的右节点3,输出3,节点3有左节点,输出5,然后是节点3的右节点6输出,节点6有左节点8,输出8;所以打印的结果就是{1,2,4,7,3,5,6,8}
- 前序遍历代码:
public static void prePrint(TreeNode root) {
if (Objects.nonNull(root)) {
System.out.println(root.val);
midPrint(root.left);
midPrint(root.right);
}
}
- 中序遍历代码:
public static void midPrint(TreeNode root) {
if (Objects.nonNull(root)) {
midPrint(root.left);
System.out.println(root.val);
midPrint(root.right);
}
}
- 后序遍历代码:
public static void afterPrint(TreeNode root) {
if (Objects.nonNull(root)) {
midPrint(root.left);
midPrint(root.right);
System.out.println(root.val);
}
}
我们可以看到,所谓前中后序遍历二叉树,就是当前节点的打印时间;
请您一定要理解前、中、后序遍历,这是二叉树的基础;如果不明白,建议debug跟断点,主要是理解递归的流程;
明确了前序遍历和中序遍历之后,我们想一下,前序遍历中的第一个输出的节点,是什么节点,没错,它就是整个二叉树的根节点,因为,前序遍历的过程中,根节点首先被输出,再依次输出它的左子树和右子树;
那么根据中序遍历的特性,根节点会在什么位置输出呢?答案是中间的某个位置,因为中序遍历会先输出根节点的左子树,在输出根节点,再输出根节点的右子树;
所以我们这件事请可以明确,前序遍历就是先是当前节点,接着是这个节点的左子树的数组,然后是右子树的数组;就好比,示例,1是根节点{2,4,7}就是左子树{3,5,6,8}就是右子树;中序遍历的先是左子树,再是当前节点,再是右子树,示例中的中序遍历结果就是{4,7,2}是左子树,1是当前根节点,{5,3,8,6}是右子树;
换句话说,给你一个前序遍历,你马上就能找到第一个节点1是根节点,然后拿着根节点1可以在中序遍历中找这个节点1的位置,比如实例中,节点1在的位置是3,那么我能得到的信息是,根节点1的左子树有三个元素{4,7,2},右子树有四个元素(因为我们知道中序遍历的数组总共多长){5,3,8,6},那么前序遍历中节点1的后三个元素就是左子树{2,4,7},再后四个就是右子树{3,5,6,8};这个就注定重建二叉树,元素不能重复,因为重复了你就不能根据前序遍历的第一个节点取确认它在中序遍历中的位置;
然后我们拿着节点1的左子树递归进行如上操作,并把它赋予给节点1的左子树引用,右子树同理;因为上面我们已经确定了,中序遍历和前序遍历的左右子树在整个遍历结果中的区间,就是索引区间值,每次递归的时候都指定中序遍历和前序遍历的当前树的区间就好;
为了避免每次我们都需要根据前序遍历的第一个元素去找中序遍历中该元素的位置,我们可以先把整个中序遍历的结果存到哈希表中,这样每次直接get即可;
当你第一次写完代码的时候,十有八九递归的四个索引值会有错的,没有关系,debug去调试,就好了,每次给它当前节点的左子树和右子树的索引区间就好了;
代码(JAVA实现)
ps:这里笔者使用的jdk为1.8版本
public class Offer07_BuildTree {
public static void main(String[] args) {
TreeNode t = buildTree(new int[]{1, 2}, new int[]{2, 1});
System.out.println();
}
public static TreeNode buildTree(int[] preorder, int[] inorder) {
if (Objects.isNull(preorder) || preorder.length == 0) {
return null;
}
// 这步是借助哈希表存储中序遍历的结果,方便每次递归的时候以O(1)的时间复杂度找到当前根节点在中序遍历数组中的位置
Map<Integer, Integer> index = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
index.put(inorder[i], i);
}
return buildTree(preorder, inorder, 0, preorder.length - 1, 0, inorder.length - 1, index);
}
public static TreeNode buildTree(int[] preorder, int[] inorder, int preorderStart, int preorderEnd
, int inorderStart, int inorderEnd, Map<Integer, Integer> index) {
// 这步是防止没有左节点和右节点导致的数组越界异常
if (preorderStart > preorderEnd) {
return null;
}
int root = preorder[preorderStart];
TreeNode treeNode = new TreeNode(root);
// 说明此时左子树或者右子树只有一个节点了,直接把这个节点返回,然后挂到父节点的左指针或者右指针上即可
if (preorderStart == preorderEnd) {
return treeNode;
} else {
int i = index.get(root);
int leftLength = i - inorderStart;
// 这步是关键,需要制定此次遍历的子树的左子树在中序和前序遍历中在各自数组中的前后坐标
treeNode.left = buildTree(preorder, inorder, preorderStart + 1, preorderStart + leftLength
, inorderStart, i - 1, index);
treeNode.right = buildTree(preorder, inorder, preorderStart + leftLength + 1, preorderEnd
, i + 1, inorderEnd, index);
return treeNode;
}
}
}