树的常见遍历方式有:前序遍历,中序遍历,后序遍历,层序遍历,关于这些遍历方式的递归和非递归实现:https://blog.csdn.net/Applying/article/details/84982712
现在,如果给定前中后序遍历方式中的两种,如何重建二叉树呢?
1. 从前序和中序遍历构建二叉树(LeetCode 105)
这个是三种情况中,最常被问到的题目。LeetCode上也有对应的题目:(LeetCode 105)
思路:对于前序遍历,其顺序为 (根值)(先序遍历左子树)(先序遍历右子树) 对于中序遍历,其顺序为 (中序遍历左子树)(根值)(中序遍历右子树) 由于题目中默认,节点值是不会重复的,所以我们可以通过按顺序遍历中序遍历的结果,找到根植对应的下标,将中序遍历成功分成三个部分。然后,根据中序遍历切出来的第一部分的左子树遍历结果的长度,来切先序遍历中左子树的部分。
实现代码:
private static TreeNode buildTree(int[] preorder, int[] inorder) {
// 判断输入是否为空,是否合法
if (preorder == null || inorder == null || preorder.length != inorder.length) return null;
return buildTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
// 注意,这里都是用下标表示
private static TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
if (preEnd < preStart || inEnd < inStart) return null;
// 从先序遍历的数组中,获取根植
TreeNode root = new TreeNode(preorder[preStart]);
// 从中序数组中寻找根植对应的下标
int i = inStart;
for (; i <= inEnd; i++) {
if (inorder[i] == root.val)
break;
}
// 利用中序遍历的特点,将左子树跟右子树分开
root.left = buildTree(preorder, preStart + 1, preStart + (i - inStart), inorder, inStart, i - 1);
root.right = buildTree(preorder, preStart + (i - inStart) + 1, preEnd, inorder, i + 1, inEnd);
return root;
}
这个解法还可以进一步利用map的数据结构进行优化:
private static TreeNode buildTree_(int[] preorder, int[] inorder) {
if (preorder == null || inorder == null || preorder.length != inorder.length) return null;
// 将中序遍历的结果放置到map结构中,方便通过根植找下标
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildTree(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1, map);
}
private static TreeNode buildTree(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd, Map<Integer, Integer> map) {
if (preEnd < preStart || inEnd < inStart) return null;
TreeNode root = new TreeNode(preorder[preStart]);
int i = map.get(root.val);
root.left = buildTree(preorder, preStart + 1, preStart + (i - inStart), inorder, inStart, i - 1, map);
root.right = buildTree(preorder, preStart + (i - inStart) + 1, preEnd, inorder, i + 1, inEnd, map);
return root;
}
这里最重点就是如何将先序遍历preorder切出其左右子树部分,和中序遍历inorder如何切出左右子树部分,我是这么考虑的:
中序遍历比较容易进行切割,我们通过找根植下标 i,因此,中序遍历 inorder 中,左子树的范围[inStart, i - 1],右子树的范围[i + 1, inEnd]。接下来,看先序遍历,根据先序遍历(根值)(先序遍历左子树)(先序遍历右子树),以及我们可以根据已经切分出来的中序遍历中的左子树的范围来推导先序遍历中左子树的范围,因此,先序遍历 preorder 中,左子树的范围[preStart + 1, preStart + (i - inStart)],其中,i - inStart 为中序遍历中左子树的长度范围。右子树的范围:[preStart + (i - inStart) + 1, preEnd]。
2. 从后序和中序遍历构建二叉树(LeetCode 106)
这个问题对应 LeetCode 106,题目如下:
思路:这种情况跟第一种类似,同样利用中序,对后序进行切分,实现代码:
private static TreeNode buildTree(int[] inorder, int[] postorder) {
if (inorder == null || postorder == null || inorder.length != postorder.length) return null;
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < inorder.length; i++) {
map.put(inorder[i], i);
}
return buildTree(0, inorder.length - 1, 0, postorder.length - 1, inorder, postorder, map);
}
private static TreeNode buildTree(int inStart, int inEnd, int postStart, int postEnd,
int[] inorder, int[] postorder, Map<Integer, Integer> map) {
if (inEnd < inStart || postEnd < postStart) return null;
TreeNode root = new TreeNode(postorder[postEnd]);
int i = map.get(root.val);
root.left = buildTree(inStart, i - 1, postStart, postStart + (i - inStart) - 1, inorder, postorder, map);
root.right = buildTree(i + 1, inEnd, postStart + (i - inStart), postEnd - 1, inorder, postorder, map);
return root;
}
中序遍历 (中序遍历左子树)(根值)(中序遍历右子树) ,而后序遍历 (后序遍历左子树)(后序遍历右子树)(根值) ,类比第一种情况,进行切分即可
3. 从前序和后序遍历构建二叉树(LeetCode 889)
该情况对应LeetCode 889,题目如下:
这种情况,因为没有中序遍历,对左右子树进行切分,因此出现了可能存在多个符合答案的解。
这里是找其中一个解即可。因此,我的思路是:根据先序 (根值)(先序遍历左子树)(先序遍历右子树) ,后序 (后序遍历左子树)(后序遍历右子树)(根值), 我假设左子树存在,则先序中 preorder[preStart] 为左子树的根植,而在后序遍历中,左子树的根植,将在最后被遍历到,也就是,它所在的位置是(后序遍历左子树)部分中的最后一个元素,因此,可以切割左子树的范围,对应右子树的范围也就出来了。
private static TreeNode constructFromPrePost(int[] pre, int[] post) {
if (pre == null || post == null || post.length != pre.length) return null;
return constructTree(0, pre.length - 1, 0, post.length - 1, pre, post);
}
private static TreeNode constructTree(int preStart, int preEnd, int postStart, int postEnd,
int[] pre, int[] post) {
if (preEnd < preStart || postEnd < postStart) return null;
TreeNode root = new TreeNode(pre[preStart]);
if (preEnd == preStart) return root;
int i = postStart;
// 找到后序遍历中,左子树的根植的位置
for (; i <= postEnd; i++) {
if (post[i] == pre[preStart + 1])
break;
}
root.left = constructTree(preStart + 1, preStart + 1 + (i - postStart), postStart, i, pre, post);
root.right = constructTree(preStart + 1 + (i - postStart) + 1, preEnd, i + 1, postEnd - 1, pre, post);
return root;
}
但,可能有人会说,可能会存在没有左子树的情况,例如:
确实,但按照我们上面的算法,[3, 8, 9] 这一部分,将被划分成左子树的部分,而后子树为 null:
对于这两种情况,其前序,后序遍历是完全相同的,即,对于求存在的其中一个符合条件的解,这种思路是可行的。