输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
限制:
0 <= 节点个数 <= 5000
首先要明确最重要的一个知识:
对于任意一颗树而言,前序遍历的形式总是
[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]
即根节点总是前序遍历中的第一个节点。而中序遍历的形式总是
[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
显然:
根据上面的特性,我们可以做出互补:
重复上述过程,我们也就可以通过将每个节点视作根节点,不断递归生成左右子树,无法再生成左右子树。很显然生成左右子树的过程可以用递归思想来实现。
思路有了,仍需解决几个问题:
先解决第一个问题:
普通的方法当然是拿着根节点的值,从中序遍历结果数组inorder [0]
开始遍历,但是每次在生成根节点时都进行遍历的话,时间复杂度较高(O(N))。因此可以使用哈希表来建设中序遍历数组值到下标的键值对映射。
在构造二叉树的过程之前,我们可以对中序遍历的列表进行一遍扫描(O(N)),就可以构造出这个哈希映射。
在此后构造二叉树的过程中,我们就只需要 O(1)的时间对根节点进行定位了。(一次O(N),N次O(1));否则我们必须每次都遍历一遍中序遍历结果数组定位根节点(N次O(N))。
再来说第二个问题:
递归生成左右子树这种说法听起来还是太“模糊”了,其实我们实际做的操作是不停的生成根节点,再进入这个根节点的左右子树,在每个子树中生成当前子树的根节点,直到这个”根节点“没有子树为止。
写代码的时候没有子树应该返回空指针——return nullptr;
,粗心大意写成了return null;
,null和nullptr是有区别的。
class Solution {
private:
map<int, int> index; // 映射值给定值对应的下标
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int num = inorder.size();
for (int i = 0; i < num; i++)
{
index[inorder[i]] = i;
// 建立中序遍历数组 值到下标 的键值对映射,快速定位根节点
}
return buildRoot(preorder, inorder, 0, num - 1, 0, num - 1);
}
// 把每个节点都当作它自身的“根节点”,进入到每个节点遍历生成它的左右子树、及根节点本身
TreeNode* buildRoot(vector<int>& pre, vector<int>& in, int pre_left,
int pre_right, int in_left, int in_right) {
if (pre_left > pre_right) { // 没有子树
return nullptr;
}
int pre_root = pre_left; // 前序遍历的根节点就是左边界
int in_root = index[pre[pre_left]]; // 根据映射关系确定中序遍历中的根节点
TreeNode* root = new TreeNode(in[in_root]); // 建立根节点
// 等价于TreeNode root = TreeNode(in[in_root]);
// TreeNode *proot = &root;
// 但没必要这样写,可能便于理解但是过于繁琐
int lefttree_num = in_root - in_left; // 确定左子树中节点数目
// 前序遍历 根节点(左边界)+1 到 根节点+左子树数量 的范围为左子树
// 中序遍历根节点左侧为左子树
root->left = buildRoot(pre, in, pre_left + 1, pre_left + lefttree_num,
in_left, in_root - 1);
// 前序遍历 根节点+左子树数量+1 到 右边界 的范围为右子树
// 中序遍历根节点右侧为右子树
root->right = buildRoot(pre, in, pre_left + 1 + lefttree_num,
pre_right, in_root + 1, in_right);
return root;
}
};
时间复杂度:O(n),其中 n 是树中的节点个数。
空间复杂度:O(n),除去返回的答案需要的 O(n)空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h < n,所以总空间复杂度为 O(n)。
前序遍历的相邻节点 u 和 v 有如下关系:
preorder = [3, 9, 8, 5, 4, 10, 20, 15, 7]
inorder = [4, 5, 8, 10, 9, 3, 15, 20, 7]
可以看到,对于3,9,8,5,4它们之间满足第一种关系(例如:8是9的左儿子),对于4,10它们满足第二种关系,10是4祖父节点的右儿子。
也就是前序遍历会
那么我们可以根据这一特性,我们可以用一个栈来存储祖先节点和左子树,直到左子树被遍历完,(本例中,将3,9,8,5,4依次入栈,直到遇到10)此时开始寻找当前节点(10)是谁的右儿子。
思路有了,仍需解决几个问题:
这时来看中序遍历,我们可以发现,中序遍历结果数组的首元素是——根节点不断往左走达到的最终节点。 根据这一特性,我们可以创建一个指针 index 指向当前的最左子树。
首先我们将根节点 3 入栈,再初始化 index 指向的节点为 4,随后对于前序遍历中的每个节点,我们依次判断它是栈顶节点的左儿子,还是栈中某个节点的右儿子。
stack = [3, 9]
index -> inorder[0] = 4
stack = [3, 9, 8, 5, 4]
index -> inorder[0] = 4
这是因为栈中的任意两个相邻的节点,前者都是后者的某个祖先。并且我们知道,栈中的任意一个节点的右儿子还没有被遍历过(前序遍历顺序——中左右),说明后者一定是前者左儿子,那么后者就先于前者出现在中序遍历中(中序遍历顺序——左中右)。
因此我们可以先把此时的栈顶元素保存并弹出, 然后把 index 不断向右移动,并与栈顶节点进行比较。如果 index 对应的元素恰好等于栈顶节点,那么说明上一个被弹出的节点没有右子树,且其本身是当前节点的左子树, 所以重复将栈顶节点保存并弹出,然后将 index 增加 1 的过程,直到 index 对应的元素不等于栈顶节点,此时 index 对应的元素就是上一个被保存且弹出的栈顶节点的右子树。按照这样的过程,我们弹出的最后一个节点 x 就是 10 的父节点,这是因为 10 出现在了
x
与x在栈中的下一个节点
的中序遍历之间,因此 10 就是 x 的右儿子(根据中序遍历顺序——左中右,x是中,10是右,x在栈中的下一个节点
是x
的父节点)。
回到我们的例子,我们会依次从栈顶弹出 4,5 和 8,并且将 index 向右移动了三次。我们将 10 作为最后弹出的节点 8 的右儿子,并将 10 入栈。
stack = [3, 9, 10]
index -> inorder[3] = 10
stack = [20]
index -> inorder[6] = 15
stack = [20, 15]
index -> inorder[6] = 15
stack = [7]
index -> inorder[8] = 7
此时遍历结束,我们构造出了正确的二叉树。
总结来讲就是,遍历前序遍历结果数组并将其压到栈中:
栈顶元素
不等于index指向的元素
时,将当前元素
作为栈顶元素
的左儿子,然后当前元素入栈成为新栈顶。栈顶元素
等于index指向的元素
时,弹出并保存栈顶元素,同时将index递增1,再判断栈顶元素和index指向的元素之间的关系,相等则重复上述操作,不相等则将当前元素
作为最后一个被弹出的栈顶元素
的右儿子,然后将当前元素入栈成为新栈顶。class Solution2 { // 迭代
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (!preorder.size()) {
return nullptr;
}
stack<TreeNode*> st;
TreeNode* root = new TreeNode(preorder[0]); // 建立根节点
st.push(root);
// 根节点入栈
// 否则无法进行将节点归为左儿子或者右儿子的操作
// 因为进行上面的操作需要访问栈顶元素的left或者right
int index = 0;
for (size_t i = 1; i < preorder.size(); i++) {
int pre = preorder[i];
int in = inorder[index];
auto node = st.top();
if (node->val != in) {
// 如果前序遍历i位置的数和中序遍历index位置的数不相等
// 说明i位置的数是二叉树的左子树
node->left = new TreeNode(pre);
st.push(node->left);
}
else {
while (!st.empty() && in == st.top()->val) {
in = inorder[++index];
node = st.top();
// 保存弹出的节点
// 当跳出while时,pre的值即为该节点右子树
st.pop();
}
node->right = new TreeNode(pre);
st.push(node->right);
}
}
return root;
}
};
时间复杂度:O(n),其中 n 是树中的节点个数。
空间复杂度:O(n),除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(h)(其中 h 是树的高度)的空间存储栈。这里 h < n,所以(在最坏情况下)总空间复杂度为 O(n)。