07、重建二叉树
题目:
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
分析:
首先分析前序遍历和中序遍历的特点:前序遍历—>根左右; 中序遍历---->左根右
所以,前序遍历中,第一个数一定是根节点,又因为不含重复的数字,所以我们只要在中序遍历中找到这个数字,则在这个数字左边的数一定是左子树,右侧的数一定是右子树。
这样我们就可以递归地构建左子树和右子树,之后再和根节点相连即可。
算法思路:
递归形式:
- 递归函数形式为 build (preorder, inorder, rootindex, inleftbegin, inrightend);
- preorder 代表前序遍历数组
- inorder代表中序遍历数组
- rootindex代表根节点的索引(从0到 length - 1)
- inleftbegin 代表在中序遍历数组中,左子树的起始索引值
- inrightend 代表在中序遍历数组中,右子树的终止索引值
- 初始化递归形式为 build (preorder, inorder, 0, 0, inorder.length - 1)
- 之后,先根据根节点的索引值,拿到根节点数据,建立根节点
- 接着,在inorder数组上,在索引为 inleftbegin~~inrightend之间,查找值等于根节点值的位置,拿到索引 i
- 之后,递归构建左子树和右子树
- 左子树:在中序遍历数组上,数据必然落在 inleftbegin~~~i-1之间。而它的根节点必然就是前序遍历中根节点对应数值的下一个数。
- 递归形式:build (preorder, inorder, rootindex + 1, inleftbegin, i - 1);
- 右子树:在中序遍历数组上,数据必然落在 i+1~~~~inrightend 之间。而它的根节点必然就是在前序遍历中根节点对应数值的索引,加上上面左子树的节点数量之后对应的值。
- 递归形式:build ( preorder, inorder, rootindex + (i - inleftbegin + 1), i + 1, inrightend );
- 递归结束的条件是
- 根节点的索引超出范围了
- 中序遍历数组中左子树的起始索引大于右子树的终止索引了
- 没有在中序遍历数组中找到对应的根节点的值
- 左子树:在中序遍历数组上,数据必然落在 inleftbegin~~~i-1之间。而它的根节点必然就是前序遍历中根节点对应数值的下一个数。
- 之后,只要将左右子树和根节点连接起来即可。
具体代码:
/**
* 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[] preorder, int[] inorder) {
if(preorder == null || inorder == null){
return null;
}
return build(preorder,inorder,0,0,inorder.length-1);
}
public TreeNode build(int[] preorder,int[] inorder, int rootindex, int inleftbegin, int inrightend){
if(inleftbegin>inrightend || rootindex >= preorder.length){
return null;
}
TreeNode root = new TreeNode(preorder[rootindex]); //根节点
//在中序遍历中寻找根节点
int i = inleftbegin;
while(i<=inrightend && inorder[i] != preorder[rootindex]){
i++;
}
if(i>inrightend){ //没有找到
return null;
}
TreeNode left = build(preorder,inorder,rootindex+1,inleftbegin,i-1);
TreeNode right = build(preorder,inorder,rootindex+(i-inleftbegin+1),i+1,inrightend);
root.left = left;
root.right = right;
return root;
}
}
进一步优化:
可以发现,在上面的代码中,每次都要进行大量的遍历才能够找到根节点在中序遍历数组中的位置,这会浪费很多时间。
所以,借助于每个节点的值都不相同这个特性,我们可以先进行一次遍历,将中序遍历数组中每个值作为键,将它对应的索引作为值,以键值对的形式存储起来,这样在查找一个数值时就是O(1)的时间复杂度了。
但是,提高了算法的空间复杂度。
代码如下:
/**
* 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[] preorder, int[] inorder) {
if(preorder == null|| inorder == null){
return null;
}
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i],i);
}
return build(preorder,inorder,map,0,0,inorder.length-1);
}
public TreeNode build(int[] preorder,int[] inorder, Map<Integer,Integer> map, int rootindex, int inleftbegin, int inrightend){
if(inleftbegin>inrightend || rootindex >= preorder.length){
return null;
}
TreeNode root = new TreeNode(preorder[rootindex]); //根节点
//在中序遍历中寻找根节点
Integer ii = map.get(preorder[rootindex]);
if(ii == null){
return null;
}
int i = ii;
TreeNode left = build(preorder,inorder,map,rootindex+1,inleftbegin,i-1);
TreeNode right = build(preorder,inorder,map,rootindex+(i-inleftbegin+1),i+1,inrightend);
root.left = left;
root.right = right;
return root;
}
}
非递归形式(栈实现):
思路:
- 其实和递归的思路是一样的,只是通过栈结构来实现而已。因为所有的递归必然可以等价地通过一个栈来实现!!!
- 其中关键的地方可能就是数据在栈里面如何存放的问题。就是什么数据要进栈,如何进栈出栈?如何对应?
- 这里采用两个栈来实现。
- 第一个栈里面存放树的节点,栈顶元素就是当前要处理的当前树的根节点。
- 第二个栈内主要存放当前这个树的所有节点在前序和中序遍历数组中的位置范围。
- 通过四个数字来表示范围。
- 栈顶的四个数就对应着目前第一个栈中栈顶元素所对应的子树的所有节点的范围。
- 这两个栈中的元素要互相对应。
- 当栈为空时,就代表已经处理完毕了。
思路可能写的不是特别清晰,直接看代码可能更清楚一点………………
代码:
/**
* 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[] preorder, int[] inorder) {
// 判断数组是否为空时,一定要判断两个条件,一是数组的引用是否为null,二是数组的长度是否为0
if(preorder == null || preorder.length == 0 ||
inorder == null || inorder.length == 0){
return null;
}
// 存放中序遍历值和脚标的对应关系,提高查找效率
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0; i<inorder.length; i++){
map.put(inorder[i],i);
}
Stack<Integer> p = new Stack<>(); //存放位置信息
Stack<TreeNode> nodes = new Stack<>(); //存放节点
TreeNode root = new TreeNode(preorder[0]); // 建立根节点
nodes.push(root); //入栈
// 以当前nodes栈中栈顶节点为根节点的树,该树上的所有节点在中序和前序遍历数组内的位置范围
// 初始情况肯定就是包含所有的元素
p.push(0);
p.push(preorder.length - 1);
p.push(0);
p.push(inorder.length - 1);
while(!nodes.isEmpty()){ // 循环结束条件是栈为空
int inend = p.pop(); // 先取出位置信息,注意顺序要和入栈顺序相反
int inbegin = p.pop();
int preend = p.pop();
int prebegin = p.pop();
TreeNode curroot = nodes.pop(); //取出当前的根节点
curroot.val = preorder[prebegin]; //当前根节点对应的值必然是当前前序遍历的第一个元素
int inindex = map.get(preorder[prebegin]); //得到当前根节点在中序遍历中的索引位置
int leftlen = inindex - inbegin; // 当前根节点的左子树上的所有节点个数
if(leftlen > 0){ // 大于零,说明存在左子树
TreeNode leftNode = new TreeNode(-1); //建立左子树根节点
curroot.left = leftNode; // 和当前的根节点建立连接
nodes.push(leftNode); // 左节点入栈
// 将左节点leftNode视为根节点时,其对应树上的所有节点在两个数组中的位置范围
// 也就是当前根节点curroot对应的左子树上所有节点在两个数组中的位置范围
p.push(prebegin+1);
p.push(prebegin+leftlen);
p.push(inbegin);
p.push(inindex-1);
}
int rightlen = inend - inindex; // 当前根节点的右子树的节点个数
if(rightlen > 0){ // 大于零,说明存在右子树
TreeNode rightNode = new TreeNode(-1); //建立右子树根节点
curroot.right = rightNode;
nodes.push(rightNode); //右节点入栈
p.push(prebegin+leftlen+1); //curroot右子树的所有节点对应的位置范围
p.push(preend);
p.push(inindex+1);
p.push(inend);
}
}
return root;
}
}
非递归形式(队列实现)
写完栈的非递归实现之后,观察上面的代码可以发现,其实我们在整个构建的过程中,节点入栈之后,每一个节点之间其实并没有很强的依赖关系。哪一个节点先进行构建貌似并没有很大差别。
这就催生了另一个相法,如果我不用栈结构,使用队列结构可以实现吗?
答案是肯定的!
其实,采用栈结构,相当于我们通过深度优先的思路在进行树的重建,那自然我们也可以根据广度优先来进行树的重建呀。
采用广度优先,就对应着使用队列结构来进行树的重建。
代码如下(和上面类似,改变的仅仅是将栈换成了队列):
/**
* 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[] preorder, int[] inorder) {
if(preorder == null || preorder.length == 0 || inorder == null || inorder.length == 0){
return null;
}
Map<Integer,Integer> map = new HashMap<>(); // 存放中序遍历值和脚标的对应关系
for(int i = 0; i<inorder.length; i++){
map.put(inorder[i],i);
}
Queue<Integer> p = new LinkedList<>(); //存放位置信息
Queue<TreeNode> nodes = new LinkedList<>(); //存放节点
TreeNode root = new TreeNode(preorder[0]);
nodes.offer(root);
p.offer(0);
p.offer(preorder.length - 1);
p.offer(0);
p.offer(inorder.length - 1);
while(!nodes.isEmpty()){
int prebegin = p.poll();
int preend = p.poll();
int inbegin = p.poll();
int inend = p.poll();
TreeNode curroot = nodes.poll(); //取出当前的根节点
curroot.val = preorder[prebegin]; //当前根节点对应的值必然是前序遍历的当前第一个元素
int inindex = map.get(preorder[prebegin]); //得到当前根节点在中序遍历中的索引位置
int leftlen = inindex - inbegin; // 当前节点的左子树的所有节点个数
if(leftlen > 0){ // 大于零,说明存在左子树
TreeNode leftNode = new TreeNode(-1); //建立左子树根节点
curroot.left = leftNode; // 和当前的根节点建立连接
nodes.offer(leftNode); // 左节点入队列
p.offer(prebegin+1); // 左节点作为根节点时,对应的树的节点在两个遍历数组中的位置
p.offer(prebegin+leftlen);
p.offer(inbegin);
p.offer(inindex-1);
}
int rightlen = inend - inindex; // 当前根节点的右子树的节点个数
if(rightlen > 0){ // 大于零,说明存在右子树
TreeNode rightNode = new TreeNode(-1); //建立右子树根节点
curroot.right = rightNode;
nodes.offer(rightNode);
p.offer(prebegin+leftlen+1);
p.offer(preend);
p.offer(inindex+1);
p.offer(inend);
}
}
return root;
}
}