最近重新回归leetcode,今天写了一下105题,虽然以前在nowcoder上刷过这道题,但是当时仅仅只是完成任务而已,没有过多的优化操作,所以,今天对常规思路和优化思路都做一
个总结。
这道题的大体意思就是说用一个二叉树的前序和中序来恢复这个二叉树,我们以题目中的测试用例为例进行说明。这样要点就有两个,第一,需要知道正确的指向顺序,因为需要恢复二叉树,所以,第二,需要创建节点。
方法一:
以题目中的例子为例进行说明,前序遍历结果是: [3,9,20,15,7] 。中序遍历是:[9,3,15,20,7]。
根据前序遍历结果,3对应的点是整个数的根节点,而根据中序遍历结果,3对应节点的左子树只有一个节点:9,右子树有三个值:15, 20, 7。即:
根节点:3
左子树:9
右子树:15、20、7
因为左子树只有9,所以,左子树的长度是1,其后都是空(NULL),无需继续遍历,然后看右子树,根据前序遍历结果,20就是一个根节点,再从中序遍历中找到根节点20,对应的左子树为15,右子树为7 。即:
根节点:20
左子树:15
右子树:7
因为每个子树都只有一个元素,因此,迭代停止。
最终我们得到原始的二叉树,如图所示:
以下是根据该思路完成的C++代码,运行时间大约是8ms
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.empty()||inorder.empty()||(preorder.size()!=inorder.size())){
return NULL;
}
int prestart = 0;
int preend = preorder.size();
int instart = 0;
int inend = inorder.size();
return (buildTree(preorder, prestart, preend, inorder, instart, inend));
}
TreeNode* buildTree(vector<int>& pre, int prestart, int preend, vector<int>& in, int instart, int inend){
if( prestart>=preend || instart>=inend){
return NULL;
}
int rootVal = pre[prestart];
int rootIdx = 0;
for(int i=instart; i<inend; i++){
if(rootVal == in[i]){
rootIdx = i;
break;
}
}
TreeNode* root = new TreeNode(rootVal);
int inLen = rootIdx-instart;
root->left = buildTree(pre, prestart+1, prestart+inLen+1, in, instart, rootIdx);
root->right = buildTree(pre, prestart+inLen+1, preend, in, rootIdx+1, inend);
return root;
}
};
因为自己不是很懂优化的知识,也是刚刚接触,所以,分析的不一定正确,如有问题,可以一起讨论,上述代码的思想其实很简单,从前序遍历中找到一个根节点,然后在中序遍历数组中找到对应的根节点的左子树和右子树所对应的点分别由哪些,逐渐往下进行,直到将序列遍历完为止,整个过程其实使用到了递归的思想,因为递归的原因,运行效率可能会打折扣,而且,每次都是在前序遍历中固定一个点,在中序遍历中寻找相同值的点,这样的遍历每一次都会发生,实际上很多点都被遍历了无数遍,如果要是一次遍历就能解决问题,而不是每次都要将数组遍历一遍,所以,我参考了leetcode上运行效率最高的代码的方法,做了整理。以下是方法二的说明。
方法二,我们做这样一个假设,如果我们需要恢复的二叉树只有左子树,那么前序遍历和中序遍历的结果正好相反,我们只需要将其中一个数列倒着回去就能将两个数列匹配,如果二叉树只有右子树,那么前序遍历和中序遍历的结果是相同的,所以,只有当一个节点既有左子树又有右子树的时候,才会出现顺序差异,才会出现比较难处理的问题,否则我们直接用顺序读取即可。所以,找到那些既有左子树又有右子树的点就成了一个比较好的切入点。
在方法一中,我们是在前序遍历中固定一个点,在中序遍历中去找该点,但这样会造成中序遍历中的元素被重复遍历,增加了冗余度,因此,假设我们每次遍历的值都保存下来,然后通过匹配的方式避免重复遍历。具体的做法是这样的:
我们先从中序遍历中取出第一个点,然后在前序遍历中找寻该元素,将每一个在前序遍历中读到的元素都压入栈中,当读取到与中序遍历中第一个元素相同的元素时,说明最左边子树的元素已经读取完,而这几个元素已经存放在了栈中,此时我们需要做的是弹栈,同时与中序遍历中的元素值进行对比,因为刚刚说到,只有既有左子树又有右子树的点才会出现顺序不一致的情况,否则,前序遍历和中序遍历正好相反,如果按照弹栈的顺序,正好相同,弹栈的同时,中序遍历的索引值不断+1,直到出现不相等元素时,说明找到了既有左子树又有右子树的点,而此时,中序遍历读取到的值为该点右子树的左端点,因此,需要继续从前序遍历刚才的索引值开始寻找中序遍历中此时读取到的值,当读取到相等时,就将当前读取到的左树连接到上一次迭代结束点的右子树上。程序如下所示:
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.empty()||inorder.empty()||(preorder.size()!=inorder.size())){
return NULL;
}
stack<TreeNode*> Treestack;
TreeNode* newNode = new TreeNode(0);
TreeNode* root = newNode;
int prestart = 0;
int preend = preorder.size();
int instart = 0;
int inend = inorder.size();
while(instart < inend){
TreeNode* leftNode = NULL;
TreeNode* leftNodehead = NULL;
do{
TreeNode* pnewNode = new TreeNode(preorder[prestart]);
if(leftNode==NULL && leftNodehead==NULL){
leftNode = pnewNode;
leftNodehead = leftNode;
}
else{
leftNode->left = pnewNode;
leftNode = leftNode->left;
}
Treestack.push(leftNode);
}
while(preorder[prestart++] != inorder[instart]);
root->right = leftNodehead;
TreeNode* matchingNode = NULL;
while(!Treestack.empty()){
TreeNode* topNode = Treestack.top();
if(topNode->val == inorder[instart]){
Treestack.pop();
matchingNode = topNode;
instart++;
}
else{
break;
}
}
root = matchingNode;
}
return newNode->right;
}
};