输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。
假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
示例1:
Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]
Output: [3,9,20,null,null,15,7]
对于任意一颗树而言,前序遍历时根节点总是前序遍历中的第一个节点,因此只要在中序遍历中定位到根节点,那么就可以分别知道左子树和右子树中的节点数目。由于同一颗子树的前序遍历和中序遍历的长度显然是相同的,因此就可以对应到前序遍历的结果中,对左右子树进行划分,进而就可以得到左右子树的前序遍历和中序遍历结果,就可以递归地对构造出左子树和右子树,再将这两颗子树接到根节点的左右位置。
细节:
在中序遍历中对根节点进行定位时,一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。因此可以考虑使用哈希表来对根节点进行快速定位。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。在构造二叉树的过程之前,可以对中序遍历的列表进行一遍扫描,就可以构造出这个哈希映射。在此后构造二叉树的过程中,就只需要 O ( 1 ) O(1) O(1) 的时间对根节点进行定位了。
class Solution {
private:
unordered_map<int, int> index;
public:
TreeNode* myBuildTree(const vector<int>& preorder, const vector<int>& inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) {
return nullptr;
}
// 前序遍历中的第一个节点就是根节点
int preorder_root = preorder_left;
// 在中序遍历中定位根节点
int inorder_root = index[preorder[preorder_root]];
// 先把根节点建立出来
TreeNode* root = new TreeNode(preorder[preorder_root]);
// 得到左子树中的节点数目
int size_left_subtree = inorder_root - inorder_left;
// 递归地构造左子树,并连接到根节点
// 先序遍历中「从 左边界+1 开始的 size_left_subtree」个元素就对应了中序遍历中「从 左边界 开始到 根节点定位-1」的元素
root->left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归地构造右子树,并连接到根节点
// 先序遍历中「从 左边界+1+左子树节点数目 开始到 右边界」的元素就对应了中序遍历中「从 根节点定位+1 到 右边界」的元素
root->right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
int n = preorder.size();
// 构造哈希映射,帮助我们快速定位根节点
for (int i = 0; i < n; ++i) {
index[inorder[i]] = i;
}
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
};
对于前序遍历中的任意两个连续节点 u u u 和 v v v,根据前序遍历的流程,可以知道 u u u 和 v v v 只有两种可能的关系:
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(!preorder.size()){ //判断是否为空
return nullptr;
}
TreeNode* root = new TreeNode(preorder[0]); //先序的首为根节点
stack<TreeNode*> stk; //建立栈
stk.push(root); //根节点入栈
int inorderIndex = 0; //扫描中序的指针
for(int i = 1; i < preorder.size(); i++){ //从先序遍历开始逐个遍历
TreeNode *node = stk.top();
if(node->val != inorder[inorderIndex]){ //栈顶元素的值与中序遍历当前所指的元素值不等
node->left = new TreeNode(preorder[i]); //前序遍历中处在栈顶元素位置后一位的元素是栈顶元素的左子树
stk.push(node->left); //栈顶元素左子树节点入栈
}else{ //栈顶元素的值与中序遍历当前所指的元素值相等,栈顶即为最左下角的树节点
while(!stk.empty() && stk.top()->val == inorder[inorderIndex]){ //while循环向上返回,寻找位置进行右子树的重建
node = stk.top(); //指针向右扫描中序遍历
stk.pop(); //栈中所有与当前指针所指元素值相等的节点出栈
inorderIndex++;
}
node->right = new TreeNode(preorder[i]); // 循环结束后,node所指栈顶元素即是需要重建右子树的节点
stk.push(node->right);
}
}
return root;
}
};
根据前序遍历和中序遍历可以确定三个节点:1.树的根节点、2.左子树根节点、3.右子树根节点。
根据「分治算法」思想,对于树的左、右子树,仍可复用以上方法划分子树的左右子树。
算法解析:
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
this->preorder = preorder;
for(int i = 0; i < inorder.size(); i++)
dic[inorder[i]] = i;
return recur(0, 0, inorder.size() - 1);
}
private:
vector<int> preorder;
unordered_map<int, int> dic;
TreeNode* recur(int root, int left, int right) {
if(left > right) return nullptr; // 递归终止
TreeNode* node = new TreeNode(preorder[root]); // 建立根节点
int i = dic[preorder[root]]; // 划分根节点、左子树、右子树
node->left = recur(root + 1, left, i - 1); // 开启左子树递归
node->right = recur(root + i - left + 1, i + 1, right); // 开启右子树递归
return node; // 回溯返回根节点
}
};
[1] https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-by-leetcode-s/
[2] https://leetcode-cn.com/problems/zhong-jian-er-cha-shu-lcof/solution/mian-shi-ti-07-zhong-jian-er-cha-shu-di-gui-fa-qin/