算法学习记录~2023.5.7~二叉树Day5~106. 从中序与后序遍历序列构造二叉树 & 105. 从前序与中序遍历序列构造二叉树 & 654. 最大二叉树


106. 从中序与后序遍历序列构造二叉树

题目链接

力扣题目链接

思路1:详细思路

主体思路是分治法

解决此问题的关键在于要很熟悉树的各种遍历次序代表的什么,最好能够将图画出来。通过不断切割,一层一层处理即可找到所有节点,直接根据如下图的思路即可。重点就是搞清顺序后如何切割的问题。
切割思路大概分为这几步:

  1. 如果数组大小为零的话,说明是空节点了
  2. 如果不为空,那么取后序数组最后一个元素作为节点元素
  3. 找到后序数组最后一个元素在中序数组的位置,作为切割点
  4. 切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  5. 切割后序数组,切成后序左数组和后序右数组(按照中序切割后的数组长度来作为切割标准)
  6. 递归处理左区间和右区间

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码

注意看注释,尤其是分割时的区间那一块,涉及到很多思考时的注意点和易错点

class Solution {
    TreeNode* traversal (vector<int>& inorder, vector<int>& postorder){
        //第一步:如果数组大小为零的话,说明是空节点了
        if (postorder.size() == 0)
            return NULL;

        //第二步:如果不为空,那么取后序数组最后一个元素作为节点元素
        int rootValue = postorder[postorder.size() - 1];
        TreeNode* root = new TreeNode(rootValue);

        //如果是叶子节点的话就不需要继续切割,把这个点存入树即可,返回当前根结点继续
        if (postorder.size() == 1)
            return root;

        //第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
        int delimiterIndex;    //切割点坐标
        for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++){
            if (inorder[delimiterIndex] == rootValue)
                break;
        }

        //第四步:切割中序数组,切成中序左数组和中序右数组(注意区间)(vector左闭右开)
        //实际的右边界应该为切割点左一个,也就是角标为delimiterIndex-1
        //结合vector语法,左闭右开,begin加的数其实就是角标,直接加切割点坐标则实际区间正好为左一个
        //[0, delimiterIndex)
        vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
        //左边界直接是第一个坐标即可,根据vector语法直接begin加坐标
        //[delimiterIndex + 1, end)
        vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());

        //第五步:切割后序数组,切成后序左数组和后序右数组(注意区间)(vector左闭右开)
        postorder.pop_back();       //后序数组抛弃末尾,因为已经用过了,也就是中间节点
        //[0, leftInorder.size)
        vector<int> leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size());
        //leftInorder角标正好是长度为leftInorder.size()的下一个的角标
        //[leftInorder.size(), end)
        vector<int> rightPostorder(postorder.begin() + leftInorder.size(), postorder.end());

        //第六步:递归处理左区间和右区间
        root -> left = traversal(leftInorder, leftPostorder);
        root -> right = traversal(rightInorder, rightPostorder);

        return root;
    }
public:

    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        if (inorder.size() == 0 || postorder.size() == 0)
            return NULL;
        return traversal(inorder, postorder);
    }
};

思路2:使用下标为参数的优化版本

写在前面:和思路1不同的是这次使用左闭右闭,同时创建一个哈希表pos来记录每个值在中序遍历中的位置。
先创建根节点,然后递归创建左右子树,并让指针指向两棵子树

A. 关于在中序遍历中对根节点进行快速定位:
一种简单的方法是直接扫描整个中序遍历的结果并找出根节点,但这样做的时间复杂度较高。我们可以考虑使用哈希表来帮助我们快速地定位根节点。对于哈希映射中的每个键值对,键表示一个元素(节点的值),值表示其在中序遍历中的出现位置。这样在中序遍历中查找根节点位置的操作,只需要 O(1) 的时间

  1. 创建一个哈希表pos记录记录每个值在中序遍历中的位置。
  2. 先利用后序遍历找根节点:后序遍历的最后一个数,就是根节点的值;
  3. 确定左右子树的后序遍历和中序遍历,先递归创建出左右子树,然后创建根节点;
  4. 最后将根节点的左右指针指向两棵子树;
    时间复杂度分析: 查找根节点的位置需要O(1) 的时间,创建每个节点需要的时间是 O(1),因此总的时间复杂度是 O(n)。
    在这里插入图片描述

B. 关于子树的左右边界:

  1. 先利用后序遍历找根节点:后序遍历的最后一个数,就是根节点的值;
  2. 在中序遍历中找到根节点的位置 k,则 k 左边是左子树的中序遍历,右边是右子树的中序遍历;
  3. 假设il,ir对应子树中序遍历区间的左右端点, pl,pr对应子树后序遍历区间的左右端点。那么左子树的中序遍历的区间为 [il, k - 1],右子树的中序遍历的区间为[k + 1, ir];
  4. 由步骤3可知左子树中序遍历的长度为k - 1 - il + 1,由于一棵树的中序遍历和后序遍历的长度相等,因此后序遍历的长度也为k - 1 - il + 1。这样,根据后序遍历的长度,我们可以推导出左子树后序遍历的区间为[pl, pl + k - 1 - il],右子树的后序遍历的区间为[pl + k - 1 - il + 1, pr - 1];

代码

class Solution {
public:
    unordered_map<int, int> pos;
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
        int n = inorder.size();
        for(int i = 0; i < n; i++){
            pos[inorder[i]] = i;     //记录中序遍历的根节点位置
        }
        return dfs(inorder, postorder, 0, n - 1, 0, n - 1);
    }
    TreeNode* dfs(vector<int>& inorder, vector<int>& postorder,int il, int ir, int pl, int pr){
        if(il > ir) return nullptr;
        int k = pos[postorder[pr]];   //中序遍历根节点位置
        TreeNode* root = new TreeNode(postorder[pr]); //创建根节点
        root->left = dfs(inorder, postorder, il, k - 1, pl, pl + k - 1 - il);
        root->right = dfs(inorder, postorder, k + 1, ir, pl + k - 1 - il + 1, pr - 1);
        return root;
    }
};

总结

分割时的区间问题是最大的难点,首先是一定要保持一致性,另外就是必须得熟悉使用的计算机语言的基础,了解诸如vector(begin(),begin()+ x)的x到底指什么(其实就正好相当于角标,从0开始),以及end()并不指向最后一个元素


105. 从前序与中序遍历序列构造二叉树

题目链接

力扣题目链接

思路

和106一致,只要想清楚遍历顺序节点有什么特殊之处,就能不断找到根结点,先创建根节点,然后递归创建左右子树,并让指针指向两棵子树

代码

class Solution {
public:
    unordered_map<int,int> pos;

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        int n = preorder.size();
        for (int i = 0; i  < n; i++)
            pos[inorder[i]] = i;   //记录中序遍历的根结点位置
        return dfs(preorder, 0, n - 1, inorder, 0, n - 1);
    }

    TreeNode* dfs(vector<int>& pre, int pl, int pr, vector<int>& in, int il, int ir){
        if ( pl > pr )
            return NULL;
        int k = pos[pre[pl]] - il; //左子树长度;pos[pre[pl]]为中序遍历中根结点位置
        TreeNode* root = new TreeNode(pre[pl]);
        root -> left = dfs(pre, pl + 1, pl + k, in, il, il + k - 1);
        root -> right = dfs(pre,pl + k + 1, pr, in, il + k + 1, ir);
        return root;
    }
};

总结


654. 最大二叉树

题目链接

力扣题目链接

思路

其实和上面两道题类似,同样是找到根节点,然后不断递归左右子树。
因此直接看代码就可以了

代码

class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return dfs(nums, 0, nums.size() - 1);   //传的是下标,所以右下标要-1
    }
    TreeNode* dfs(vector<int>& nums, int left, int right){
        if (left > right)
            return NULL;
        int maxIndex = left;        //初始化最大值的坐标
        for (int i = left + 1; i <= right; i++){    //因为right是能到达的最大坐标,所以条件要有=
            if (nums[i] > nums[maxIndex])
                maxIndex = i;
        }
        TreeNode* root = new TreeNode(nums[maxIndex]);
        root -> left = dfs(nums, left, maxIndex - 1);
        root -> right = dfs(nums, maxIndex + 1, right);
        return root;
    }
};

总结

难点还是在于区间的限制。
本次解题采用左闭右闭,传入参数为实际的下标,因此在最开始传值时不能直接传整个数组的size,而应该减1。
另外在循环条件的设置中也要考虑到right是可以达到的,所以不能用小于,而应该用小于等于。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山药泥拌饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值