剑指Offer_#7_重建二叉树
剑指Offer_#7_重建二叉树
Contents
题目
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
限制:
0 <= 节点个数 <= 5000
思路分析
整体思路
引用题解区liweiwei大佬的一个图解 题解链接
这个图片很清晰地展示了本题的思路。
整体的思路如下:
- 找到当前树的根节点。根节点一定是前序遍历序列的第一个,即上图中的1。
- 找到根节点在中序遍历序列当中的位置,即上图中pivot指向的位置。
- 根据中序遍历中根节点的位置,可以将整个
inorder
数组划分为左子树部分和右子树部分,即绿色框和红色框部分。 - 根据中序遍历中左子树部分的个数,反过来又可以把
preorder
数组划分为左子树部分和右子树部分,即绿色框和红色框部分。
以上的步骤将前序遍历序列和中序遍历序列划分成为3部分
- 根节点
- 左子树
- 右子树
但是这还不足以重构整个二叉树,因为这是个递归问题,还需要解决递归子问题
- 左子树部分还可以划分为左子树的左子树,左子树的右子树
- 右子树部分还可以划分为右子树的左子树,右子树的右子树
我们需要编写递归函数来实现上述逻辑。
递归函数
递归参数(函数签名)
TreeNode recur(int preL,int preR,int inL,int inR)
preL
,preR
表示当前子树在前序遍历序列preorder当中的左右边界inL
,inR
表示当前子树在中序遍历序列inorder当中的左右边界
递归终止条件
如果左边界大于右边界,表示当前的子树没有任何节点,是null
if(preL > preR || inL > inR) return null;
递推过程
- 构建当前子树的根节点
root
,root
的值是preorder[preL]
- 在中序遍历序列
inorder
中寻找根节点root
的值所在的位置- 方法1:提前构建一个
HashMap
,保存键值对<inorder[i],i>
,可以直接查找到 - 方法2:遍历
inorder
数组,找到root
的值
- 方法1:提前构建一个
- 根据上面的图解,我们可以划分出
root
的左右子树在两个序列里的范围。得到root
的左右子树的preL
,preR
,inL
,inR
。调用递归子函数,构造出root
的左右子树。
返回值(回溯)
返回当前构建的root
子树,成为更高一层递归函数中的左右子树。
其他细节
特殊输入
preorder
/inorder
是nullpreorder
/inorder
长度为0preorder
和inorder
长度不同
以上情况都无法重建出一个二叉树,返回null
全局变量
hashMap
变量,用于保存键值对<inorder[i],i>
po
变量,保存preorder
数组,避免递归函数参数太多
解答
代码1
本代码参考了liweiwei的实现。
class Solution {
HashMap<Integer,Integer> map = new HashMap();
//前序遍历序列的全局变量,避免递归函数的参数太多
int[] po;
public TreeNode buildTree(int[] preorder, int[] inorder) {
int preLen = preorder.length;
int inLen = inorder.length;
//特殊输入:null,空数组,两数组长度不同
if(preorder == null || inorder == null || preLen == 0 || inLen == 0 || preLen != inLen)
return null;
po = preorder;
//将中序遍历序列的<inorder[i],i>存入map,以便在inorder数组中快速找到子树根节点的值
for(int i = 0;i <= inorder.length - 1;i++){
map.put(inorder[i],i);
}
//开启递归调用
return recur(0,preLen - 1,0,inLen - 1);
}
//递归函数参数:
//preL,preR表示当前子树在前序遍历序列preorder当中的左右边界
//inL,inR表示当前子树在中序遍历序列inorder当中的左右边界
private TreeNode recur(int preL,int preR,int inL,int inR){
//递归出口条件:左边界大于右边界,含义是当前子树为空
if(preL > preR || inL > inR) return null;
//当前子树的根节点的值就是po[preL]
int pivot = po[preL];
TreeNode root = new TreeNode(pivot);
//利用map,找到当前子树根节点在中序遍历序列inorder中的索引
int pivotIndex = map.get(pivot);
//开启下一级递归调用,将程序阻塞在这里,直到满足递归终止条件
//重点在于递推过程中的4个参数,必须在纸上先画出图,推导出参数,再写代码
root.left = recur(preL + 1,preL + pivotIndex - inL,inL,pivotIndex - 1);
root.right = recur(preL + pivotIndex - inL + 1,preR,pivotIndex + 1,inR);
//回溯,将构造好的子树返回给上一级递归函数
return root;
}
}
代码2:进一步优化代码
本代码参考了K神的实现图解算法数据结构。
仔细观察,可以发现代码1当中的preR
其实没有用到,是一个没用的参数,可以直接把他去掉。
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
if(preorder.length != inorder.length) return null;
int n = preorder.length;
//使用这个map,可以根据值查找到索引
for(int i = 0; i < n; i++) map.put(inorder[i], i);
return recur(preorder, 0, 0, n - 1);
}
//递归函数的参数实际上不需要inorder数组,因为根本没有用到
private TreeNode recur(int[] preorder, int pre_lo, int in_lo, int in_hi){
//左边界大于右边界,说明这个区间内没有任何节点,返回null,也就是叶子节点
//其实只需要考虑中序遍历或者前序遍历中的一个的左右边界即可,因为实际上是等效的
if(in_lo > in_hi) return null;
int root_val = preorder[pre_lo];
int root_idx_in = map.get(root_val);
TreeNode root = new TreeNode(root_val);
root.left = recur(preorder, pre_lo + 1, in_lo, root_idx_in - 1);
root.right = recur(preorder, pre_lo + root_idx_in - in_lo + 1, root_idx_in + 1, in_hi);
return root;
}
}
递归函数参数设计的技巧
从上面这题中可以总结出一个设计递归函数参数的技巧,
- 如果不知道哪些参数必要,哪些参数非必要,可以先都写上去,最后把没有用到的删除掉即可
- 不好的方法:先把参数表空着,一边写一边填充,这样的话,就没法写子递归调用的参数了
复杂度分析
时间复杂度:O(n)
- 初始化
hashmap
,复杂度是O(n)
- 每个递归函数构建一个节点,所以递归函数调用
O(n)
次
空间复杂度:O(n)
hashmap
占用空间O(n)
- 递归调用占用空间
O(n)