LeetCode No.106 从中序与后序遍历序列构造二叉树(Medium)

从中序与后序遍历序列构造二叉树(字节二面凉经)

写在前面

其实这道题的思路并不难,他的难点在于如何优化时间复杂度。这题目给了我很深的印象,因为我之前面字节跳动的时候碰到了这道题。虽然我有思路,但是最后一个样例超时了,当时没想到优化的方法。

今天我翻出了这道题,重新实现了一遍。勉强通过了,但是时间和空间都不理想,所以特意记录一下。

题目描述


根据一棵树的中序遍历与后序遍历构造二叉树。

注意:
你可以假设树中没有重复的元素

例如,给出

中序遍历 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]不是真正的头节点,继续往下找。

代码就不贴了,有兴趣的可以尝试一下。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值