1. 题目
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
//前序遍历 preorder = [3,9,20,15,7]
//中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
2. 分析
这个题目是以前在本科的数据结构考试中经常出现的一类问题。前序遍历顺序是遍历根节点,左子树,右子树,而中序遍历则是左子树,根节点,右子树,因此这类题目的解题思路是根据前序遍历的第一个元素确定根节点,然后在中顺遍历中找到根节点的位置。在中序遍历的左侧是左子树,右侧是右子树。如上面的例子,首先我们根据前序的第一个节点确定3是根节点,那么在中序遍历结果中找到3,那么中序遍历结果中左侧的序列【9】则是3为根节点的左子树的中序结果,而右侧的序列【15,20,7】则是右子树的中序结果。
确定了左右子树,继续在左子树的中序遍历结果中找到出现在先序遍历结果的元素,因为在先序遍历结果首先出现的一定是子树的根节点。如本题,左子树的中序遍历结果为【9】,只有一个元素,那么一定是9先出现在先序的结果中,因此左子树根节点为9。右子树的中序遍历结果为【15,20,7】,那么首先出现在先序遍历结果【3,9,20,15,7】的元素是20,那么20是右子树的根节点。
因为左子树根节点9在其子树对应的中序结果【9】中没有左侧和右侧的序列,那么9则是一个叶子节点。而右子树根节点20在其对应子树的中序结果【15,20,7】中存在左侧序列【15】和右侧序列【7】,那么【15】对应的则是以20为根节点的左子树的中序结果,而【7】则是以20为根节点的右子树的中序结果。循环递归上面的过程构造子树。
上面的过程是我在数据结构考试中,笔试经常使用的解题思路,反应到程序中需要解决两个重要的问题:
1. 先序遍历结果的第一个元素(根节点)在中序遍历结果中的位置如何确定?
2. 左子树中序遍历子序列的根节点,即左子树的根节点如何确定?同样,右子树中序遍历子序列的根节点,即右子树的根节点如何确定?
3. C++程序一(版本1:leetcode提交超出时间限制)
这份代码能够通过一些简单的树的case,但是如果树的数量级太大,在leetcode的203个测试中,通过了202个,最后一个测试显示超时,因为这个算法的时间复杂度是3次方级!!!。
虽然这份程序没有通过所有测试,但我还是在这里给出,主要原因是我想通过在这份程序的思路上纠正可以优化的部分。
也许有的读者能够发现,前面的分析中,我始终只是对中序结果进行划分,而始终在原始的preorder序列中确定位置。
反观我们的代码中,首先,在中序遍历结果中确定根节点位置这个过程需要保留,而这个过程的时间复杂度是线性的。程序中主要的时间消耗在于从子树对应的中序遍历结果中确定根节点这个过程。因为需要一层循环逐个遍历中序遍历子序列的元素,然后在先序结果中逐个确定其位置,确定位置的过程也需要循环,则是两层循环,体现在下面的代码段中。
for(int i=0;i<left.size();i++){
int index = 0;
while(left[i]!=preorder[index]) index++;
if(index<minLeft){
leftVal = preorder[index];
minLeft = index;
}
}
再加上递归的过程,总的时间复杂度是,其中logn是递归的次数,即树高,n^2则是上面给出的两层循环
给出这个版本的全部代码。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.size()==0) return NULL;
TreeNode* root = new TreeNode(preorder[0]);
if(preorder.size()==1) return root;
vector<int> left,right;
int location = 0;
while(inorder[location]!=root->val){
left.push_back(inorder[location]);
location++;
}
location++;
while(location<inorder.size()){
right.push_back(inorder[location]);
location++;
}
buildChild(root,left,right,preorder);
return root;
}
void buildChild(TreeNode* curRoot,vector<int> left,vector<int> right,vector<int> preorder){
if(left.size()==0) curRoot->left = NULL;
if(right.size()==0) curRoot->right = NULL;
int leftVal,rightVal;
int minLeft=INT_MAX,minRight=INT_MAX;
vector<int> leftnew1,rightnew1,leftnew2,rightnew2;
int location = 0;
if(left.size()!=0){
int leftVal;
int minLeft=INT_MAX;
vector<int> leftnew1,rightnew1;
for(int i=0;i<left.size();i++){
int index = 0;
while(left[i]!=preorder[index]) index++;
if(index<minLeft){
leftVal = preorder[index];
minLeft = index;
}
}
TreeNode* lchild = new TreeNode(leftVal);
int location = 0;
while(left[location]!=lchild->val){
leftnew1.push_back(left[location]);
location++;
}
location++;
while(location<left.size()){
rightnew1.push_back(left[location]);
location++;
}
curRoot->left = lchild;
buildChild(lchild,leftnew1,rightnew1,preorder);
}
if(right.size()!=0){
int rightVal;
int minRight=INT_MAX;
vector<int> leftnew2,rightnew2;
for(int i=0;i<right.size();i++){
int index = 0;
while(right[i]!=preorder[index]) index++;
if(index<minRight){
rightVal = preorder[index];
minRight = index;
}
}
TreeNode* rchild = new TreeNode(rightVal);
int location = 0;
while(right[location]!=rchild->val){
leftnew2.push_back(right[location]);
location++;
}
location++;
while(location<right.size()){
rightnew2.push_back(right[location]);
location++;
}
curRoot->right = rchild;
buildChild(rchild,leftnew2,rightnew2,preorder);
}
}
};
4. C++程序二(版本2:leetcode通过程序)
然而,给定中序遍历以及先序遍历结果,能够确定唯一的树结构。那么我们除了对中序遍历结果递归划分之外,仍然可以对先序遍历结果进行递归划分。那么,除了需要确定左右子树对应的中序遍历子序列之外,如何确定先序遍历结果中左右子树各自对应的部分则是第二个问题,当子树的先序遍历结果确定之后,第一个节点则是根节点,就不需要两层循环来确定子树的根节点了。
那么,究竟是如何确定先序遍历的子树对应的子序列呢?这里面存在一个很容易发现的规律。因为我们已经确定了根节点在中序遍历结果中确定了左子树和右子树对应的中序结果,我们便同时确定了其对应的节点个数。
例如,前面的例子中,左子树对应的中序结果为【9】,那么根节点3对应的左子树则有1个节点。因为先序遍历是根->左->右的顺序,那么我们只需要在先序结果中根节点后面查1个节点,那么这个1个节点构成的序列就是左子树对应的先序结果,那么后面剩下的就是右子树对应的先序结果。具体过程如下图所示:
而递归的出口条件是当preorder的size为0,则证明这个子树已经是树叶子的NULL部分,直接返回。
给出这个版本的全部代码。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.size()==0) return NULL; //空树
TreeNode* root = new TreeNode(preorder[0]);
if(preorder.size()==1) return root; //只有一个节点
vector<int> leftIn,leftPre,rightIn,rightPre;
int location = 0;
while(inorder[location]!=root->val){
leftIn.push_back(inorder[location]);
location++;
}
for(int i=1;i<=location;i++) leftPre.push_back(preorder[i]);
for(int i=location+1;i<preorder.size();i++){
rightPre.push_back(preorder[i]);
rightIn.push_back(inorder[i]);
}
root->left = buildChild(leftPre,leftIn);
root->right = buildChild(rightPre,rightIn);
return root;
}
TreeNode* buildChild(vector<int> preorder,vector<int> inorder){
if(preorder.size()==0) return NULL; //出口条件:preorder为空,则表示这个节点是NULL
TreeNode* root = new TreeNode(preorder[0]); //生成当前子树的根节点
vector<int> leftIn,leftPre,rightIn,rightPre;
int location = 0;
while(inorder[location]!=root->val){
leftIn.push_back(inorder[location]);
location++;
}
for(int i=1;i<=location;i++) leftPre.push_back(preorder[i]);
for(int i=location+1;i<preorder.size();i++){
rightPre.push_back(preorder[i]);
rightIn.push_back(inorder[i]);
}
root->left = buildChild(leftPre,leftIn);
root->right = buildChild(rightPre,rightIn);
return root;
}
};