LeetCode刷题—重建二叉树
LeetCode中有三道很类似的题,思路也大同小异,故作一总结。
105,从前序与中序遍历序列构造二叉树,medium 106,根据中序和后序遍历构造二叉树,medium 889,根据前序和后序遍历构造二叉树,medium
这三道题都是给出两种遍历方式,由其遍历顺序可发现规律来重建此二叉树。下面详解105题,后两题思路稍作改变,比较好懂了。
105,从前序与中序遍历序列构造二叉树,medium
根据一棵树的前序遍历与中序遍历构造二叉树。
注意: 你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7] 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树:
3
/ \
9 20
/ \
15 7
方法一:分治思想 + HashMap
前序遍历:根-左-右。
中序遍历:左-根-右。
以题目示例为例:
- 前序遍历划分
[ 3 | 9 | 20 15 7 ]
- 中序遍历划分
[ 9 | 3 | 15 20 7 ]
则前序遍历的首个元素 为 根节点的值,在中序遍历数组中找到 根节点所在索引,其左边的元素为 根节点的左子树,右边的元素为 根节点的右子树。
- 前序遍历划分
以上子树的递推性质是 分治算法 的体现,考虑通过递归对所有子树进行划分。
辅助函数 build,参数为:前序遍历的起点索引 preL,结束索引 preR,中序遍历的起点索引 inL,结束索引 inR。
通过上面所述找到 root 在中序遍历的索引,递归其左边元素和 右边元素,分别赋给 root 的左子树 和 右子树。递归出口为
preL > preR || inL > inR
,表示越过叶子节点,此时返回null。其中 找到 root 在中序遍历的索引 一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用 HashMap 快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的索引。
确定 preL、preR、inL、inR 需要解方程
左子树 在 前序遍历中的起点索引:preL + 1
左子树 在 前序遍历中的结束索引:x
x 满足 x - (preL + 1) = rootIndex - 1 - intL=> x = rootIndex - intL + preL
左子树 在 中序遍历中的起点索引:intL
左子树 在 中序遍历中的结束索引:rootIndex - 1
右子树 在前序遍历中的起点索引:x + 1
右子树 在 前序遍历中的结束索引:preR
右子树 在 中序遍历中的起点索引:rootIndex + 1
右子树 在 中序遍历中的结束索引:inR
- 代码: ##### 方法二
class Solution {
private Map<Integer, Integer> map;
private int[] preorder;
public TreeNode buildTree(int[] preorder, int[] inorder) {
this.preorder = preorder;
map = new HashMap<>();
//将中序遍历数组的元素和其索引存入map键值对
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return build(0, preorder.length - 1, 0, inorder.length - 1);
}
//前序遍历:preL起始索引,preR结束索引;中序遍历:inL起始索引,inR结束索引
public TreeNode build(int preL, int preR, int inL, int inR){
if(preL > preR || inL > inR) return null;
//将前序遍历首个元素设为根节点
int rootVal = preorder[preL];
TreeNode root = new TreeNode(rootVal);
//找到中序遍历中root的索引
int rootIndex = map.get(rootVal);
//递归调用中序遍历root左右部分形成root的左右子树
root.left = build(preL + 1, rootIndex - inL + preL, inL, rootIndex - 1);
root.right = build(rootIndex - inL + preL + 1, preR, rootIndex + 1, inR);
return root;
}
} 和上面总体思想一样,但不借助 HashMap 来定位 root 在中序遍历数组的索引,而是用 List 的 indexOf 来定位,用 subList 确定左右子树的范围。
代码:
先把数组转化为list集合,然后在list集合中进行截取,这样效率明显不是很高。 #### 106,根据中序和后序遍历构造二叉树,medium 根据一棵树的中序遍历与后序遍历构造二叉树。class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
List<Integer> prelist = new ArrayList<>();
List<Integer> inlist = new ArrayList<>();
//将int数组转为List
for(int i = 0; i < preorder.length; i++){
prelist.add(preorder[i]);
inlist.add(inorder[i]);
}
return build(prelist, inlist);
}
public TreeNode build(List<Integer> prelist, List<Integer> inlist){
if(inlist.isEmpty()) return null;
//前序集合的首个元素为根节点
int rootVal = prelist.remove(0);
TreeNode root = new TreeNode(rootVal);
//找到根节点在中序的索引
int index = inlist.indexOf(rootVal);
//新的左右两边
root.left = build(prelist, inlist.subList(0, index));
root.right = build(prelist, inlist.subList(index + 1, inlist.size()));
return root;
}
}
注意: 你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7] 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树:
3
/ \
9 20
/ \
15 7
思路:和上一题的思路基本一致,由中序遍历的规律(root 左部分为左子树,右部分为右子树),后序遍历的根节点为最后的节点。
代码:
#### 889,根据前序和后序遍历构造二叉树,medium 返回与给定的前序和后序遍历匹配的任何二叉树。class Solution {
private Map<Integer, Integer> map;
private int[] postorder;
public TreeNode buildTree(int[] inorder, int[] postorder) {
this.postorder = postorder;
map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return build(0, inorder.length - 1, 0, postorder.length - 1);
}
public TreeNode build(int inL, int inR, int postL, int postR){
if(inL > inR || postL > postR) return null;
//在后序数组中找到根节点root
int rootVal = postorder[postR];
TreeNode root = new TreeNode(rootVal);
//中序中root的索引
int rootIndex = map.get(rootVal);
root.left = build(inL, rootIndex - 1, postL, postL + rootIndex - 1 - inL);
root.right = build(rootIndex + 1, inR, postL + rootIndex - inL, postR - 1);
return root;
}
}
pre 和 post 遍历中的值是不同的正整数。
示例:
输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1] 输出:[1,2,3,4,5,6,7]
思路:还是按照之前的思路,找到根节点(即前序遍历的第一个元素或后序遍历的最后一个元素),但前序和后序 不像 中序的根作为分隔点,所以需要再找左子树的根 在 后序遍历数组 的 索引,依次来确定左右子树的长度。
- 代码: #### 总结
class Solution {
Map<Integer,Integer> map;
private int[] post;
private int[] pre;
public TreeNode constructFromPrePost(int[] pre, int[] post) {
this.post = post;
this.pre = pre;
map = new HashMap<>();
for(int i = 0; i < post.length; i++){
map.put(post[i], i);
}
return build(0, pre.length - 1, 0, post.length - 1);
}
public TreeNode build(int preL, int preR, int postL, int postR){
if(preL > preR || postL > postR) return null;
//不加这步,下面int leftRoot = pre[preL + 1];会出现角标越界
if(preL == preR) return new TreeNode(pre[preL]);
//根的索引即前序第一个元素和后序最后一个元素
TreeNode root = new TreeNode(post[postR]);
//下面是为了确定左子树在数组中的长度
//左子树根节点
int leftRoot = pre[preL + 1];
//左子树根节点在后序的索引
int leftRootIndex = map.get(leftRoot);
root.left = build(preL + 1, leftRootIndex - postL + preL + 1, postL, leftRootIndex);
root.right = build(leftRootIndex - postL + preL + 2, preR, leftRootIndex + 1, postR - 1);
return root;
}
} - 三个题中 前序 + 中序 和 后序 + 中序,都利用中序的根左边部分是左子树,右边部分是右子树。所以先建立根节点,并找出根节点在中序遍历数组的索引,就可以利用此规律重建二叉树。
- 只有 前序 + 后序时,无法利用中序遍历的规律,则需要再找出左子树的根节点,来确定左、右子树的长度。
- 用 HashMap 来以空间换时间,快速定位。
注意三题都不能有重复数字,才能用上面方法。