写在前面
其实这道题的思路并不难,他的难点在于如何优化时间复杂度。这题目给了我很深的印象,因为我之前面字节跳动的时候碰到了这道题。虽然我有思路,但是最后一个样例超时了,当时没想到优化的方法。
今天我翻出了这道题,重新实现了一遍。勉强通过了,但是时间和空间都不理想,所以特意记录一下。
题目描述
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
解题思考
重新看了下题目,我发现我完全漏掉了注意事项,我想的是一个通解。即,当树中有重复元素的情况。在这道题的评论下面,大部分速度比较快的题解都是使用map
结构来存储inorder
顺序,当时我不太理解为什么,后来我才发现,因为不用考虑数据冲突,所以表结构最方便。
当然,既然我已经想出了通解,那我其实也就可以不用使用这种取巧的方法了。我发现,由于元素可能重复,此时数值 = 头节点的元素可能不止一个。虽然这样只需要增加一个简单的判断,但是我觉得对于这道题可有可无。因此,其实我的通解其实只写了一大半,剩下的可以给读者思考一下。这下面是解题思路:
- 首先我们知道,后缀遍历的最后一个元素即为树的头部,因此,
root = postorder.back()
。 - 根据中缀遍历的性质我们知道,对于一个头节点
root
,中缀遍历中,root
元素的前n
项为左子树,后m
项为右子树。那么我们可以直接将中序遍历拆成两个子树。 - 根据后序遍历的性质我们可以知道,由于后序遍历也是左子树到右子树的遍历,因此,我们可以取中序遍历拆分好的大小,直接将前后序遍历
n
项作为左子树,删掉头节点后,剩余的元素即为右子树. - 这样一来,我们就把一棵树拆成了头节点、左子树、右子树。接下来我们递归地对左子树右子树进行操作,最后,就可以完成树的构造。
- 时间复杂度应该是 O ( n l o g n ) O(nlogn) O(nlogn)
代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
TreeNode *buildTree(vector<int> &inorder, vector<int> &postorder) {
if (inorder.empty())
return nullptr;
// 从后缀表达式找到头节点
TreeNode *newNode = new TreeNode(postorder.back());
newNode->val = postorder.back();
postorder.pop_back();
// 将中序遍历按照头节点分成左右两个子树,
// 将后序遍历根据中序遍历分成的子树的元素个数分成两个子树。
// 因为后序遍历是先遍历左子树再遍历右子树的,和中序遍历一样,
// 因此可以按照中序遍历的元素个数来直接取后序遍历。
int position = distance(inorder.begin(), find(inorder.begin(), inorder.end(), newNode->val));
// 但是,对于通解来说,其实上面这条position语句有问题。
// 如果元素有重复,find找到的节点可能不是头节点。
// 那么聪明的读者,你该如何解决这个问题呢^_^
vector<int> new_inorder_left(inorder.begin(), inorder.begin() + position);
vector<int> new_postorder_left(postorder.begin(), postorder.begin() + position);
inorder.erase(inorder.begin(), inorder.begin() + position + 1);
postorder.erase(postorder.begin(), postorder.begin() + position);
// 简单的递归
if (!new_inorder_left.empty())
newNode->left = buildTree(new_inorder_left, new_postorder_left);
if (!inorder.empty())
newNode->right = buildTree(inorder, postorder);
return newNode;
}
};
int main() {
vector<int> inorder = {8, 4, 9, 2, 10, 5, 1, 6, 3, 7};
vector<int> postorder = {8, 9, 4, 10, 5, 2, 6, 7, 3, 1};
TreeNode *result;
Solution solution;
result = solution.buildTree(inorder, postorder);
cout << result;
}
总结
由此可见,我的算法能力还是很薄弱啊/(ㄒoㄒ)/~~。
当然,上面的程序注释里面有一个问题留给读者思考,那么我先给出一个参考思路:
- 找到第一个和头节点相等的元素
inorder[n]
之后,查看中序遍历和后序遍历前面的n
个元素是都出现过。 - 如果有一个或多个元素只在其中一个遍历中出现,那么说明
inorder[n]
不是真正的头节点,继续往下找。
代码就不贴了,有兴趣的可以尝试一下。