LeetCode 654.最大二叉树

给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:

创建一个根节点,其值为 nums 中的最大值。
递归地在最大值 左边 的 子数组前缀上 构建左子树。
递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。

示例 1:
在这里插入图片描述

输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:

  • [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    • [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
      • 空数组,无子节点。
      • [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
        • 空数组,无子节点。
        • 只有一个元素,所以子节点是一个值为 1 的节点。
    • [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
      • 只有一个元素,所以子节点是一个值为 0 的节点。
      • 空数组,无子节点。
        示例 2:
        在这里插入图片描述

输入:nums = [3,2,1]
输出:[3,null,2,null,1]

提示:

1 <= nums.length <= 1000
0 <= nums[i] <= 1000
nums 中的所有整数 互不相同

法一:递归构建:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        return build(nums, 0, nums.size() - 1);
    }

private:
    TreeNode *build(vector<int> &nums, int begin, int end)
    {
        if (begin > end)
        {
            return nullptr;
        }

        int max = -1;
        int maxIndex = -1;
        for (int i = begin; i <= end; ++i)
        {
            if (nums[i] > max)
            {
                max = nums[i];
                maxIndex = i;
            }
        }

        TreeNode *node = new TreeNode(max);
        node->left = build(nums, begin, maxIndex - 1);
        node->right = build(nums, maxIndex + 1, end);

        return node;
    }
};

如果nums的长度为n,此算法时间复杂度为O(n 2 ^2 2),空间复杂度为O(1)。

法二:单调栈,单调栈用于在线性时间内寻找数组中每个元素左边第一个大(小)于该值的值,或右边第一个大(小)于该值的值。简单介绍一下单调栈,我们遍历nums中的每个元素,每遍历到一个元素,如果栈顶的元素小于当前遍历到的元素,就做出栈操作,直到栈顶元素值大于当前遍历到的元素,或栈为空,这些出栈元素右边第一个大于它们的值就是当前遍历到的元素,做完出栈操作后,栈顶元素就是当前元素的左边第一个大于当前元素的值,然后再把当前遍历到的元素入栈:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        stack<int> s;
        vector<TreeNode *> nodeArr(nums.size(), nullptr);
        TreeNode *root = nullptr;
        for (int i = 0; i < nums.size(); ++i)
        {
            nodeArr[i] = new TreeNode(nums[i]);
            while (!s.empty() && nums[s.top()] < nums[i])
            {
                nodeArr[i]->left = nodeArr[s.top()];
                s.pop();
            }

            if (!s.empty())
            {
                nodeArr[s.top()]->right = nodeArr[i];
            }

            s.push(i);

            if (s.size() == 1)
            {
                root = nodeArr[i];
            }
        }

        return root;
    }
};

如果nums的长度为n,此算法时间复杂度为O(n),空间复杂度为O(n)。

法三:线段树,线段树可以logn的效率查找某区间内的最大值,将其与法一中找区间最大值的部分替换,可将找极值的时间复杂度降为logn。线段树的核心在于把数组的每一段的最大值以树来存储,如找数组3、2、1、6、0、5的极值,我们递归地处理它,第一步将其分为两部分,分别为3、2、1和6、0、5,然后极值为这两部分极值中较大的那个,继续递归处理,直到只有单个元素,此时我们就可以知道当前区间的极值了,因为只有一个元素,极值就是它本身,然后递归向上返回,即可求得每一段的极值:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        // 线段树需要开4倍内存
        nodeArr.resize(nums.size() * 4 , nullptr);

        buildSegmentTree(1, nums.size(), 1, nums);

        return buildAns(1, nums.size());
    }

private:
    TreeNode *buildAns(int left, int right)
    {
        if (left > right)
        {
            return nullptr;
        }

        pair<int, int> max = search(left, right, 1);
        int maxIndex = max.first;
        int maxValue = max.second;

        TreeNode *node = new TreeNode(maxValue);
        node->left = buildAns(left, maxIndex - 1);
        node->right = buildAns(maxIndex + 1, right);

        return node;
    }

    class Node
    {
    public:
        Node(int left, int right, int maxValue, int maxIndex) 
            : left(left), 
              right(right), 
              maxValue(maxValue),
              maxIndex(maxIndex)
        {

        }

        int left;
        int right;
        int maxValue;
        int maxIndex;
    };

    vector<Node *> nodeArr;

    void buildSegmentTree(int left, int right, int x, vector<int> &nums)
    {
        if (left > right)
        {
            return;
        }

        // 如果是叶子结点
        if (left == right)
        {
            nodeArr[x] = new Node(left, right, nums[left - 1], left);
            return;
        }

        int mid = left + (right - left) / 2;
        buildSegmentTree(left, mid, 2 * x, nums);
        buildSegmentTree(mid + 1, right, 2 * x + 1, nums);
        
        if (nodeArr[2 * x]->maxValue > nodeArr[2 * x + 1]->maxValue)
        {
            nodeArr[x] = new Node(left, right, nodeArr[2 * x]->maxValue, nodeArr[2 * x]->maxIndex);
        }
        else
        {
            nodeArr[x] = new Node(left, right, nodeArr[2 * x + 1]->maxValue, nodeArr[2 * x + 1]->maxIndex);
        }
    }

    pair<int, int> search(int left, int right, int x)
    {
        if (left == nodeArr[x]->left && right == nodeArr[x]->right)
        {
            return {nodeArr[x]->maxIndex, nodeArr[x]->maxValue};
        }

        int mid = (nodeArr[x]->left + nodeArr[x]->right) / 2;

        // 如果要查找的范围在当前节点表示范围中点的左边(包括mid,因为建线段树时mid属于左边子树)
        // 说明要查找的范围在左子树上
        // 之所以与mid做对比,是因为子树的每个节点的范围就是按mid分的
        if (right <= mid)
        {
            return search(left, right, 2 * x);
        }

        // 如果要查找的范围在当前节点表示范围中点的右边,说明要查找的范围在右子树上
        // 此处要用>而非>=,因为mid属于右子树,left只有完全属于右子树时,才只在右子树上查
        if (left > mid)
        {
            return search(left, right, 2 * x + 1);
        }
 
        // 如果跨mid,就从两个子树中找
        pair<int, int> leftMax = search(left, mid, 2 * x);
        pair<int, int> rightMax = search(mid + 1, right, 2 * x + 1);

        if (leftMax.second > rightMax.second)
        {
            return leftMax;
        }
        else
        {
            return rightMax;
        }
    }
};

如果nums的长度为n,此算法时间复杂度为O(nlogn),空间复杂度为O(n)。

法四:ST表,类似法三,ST表能以O(1)的时间查找某一段的极值。ST表的建表用时为O(nlogn),而线段树的建树用时为O(n),但查询时,ST表比线段树快得多。线段树可以动态更改节点值,而ST表不支持,因此ST表更适用于值不会被修改的情况。简单介绍一下ST表,它是一个二维数组,a[i][j]表示从i开始的2的j次方个元素的极值是多少,在查询时,我们把查询区间分为有重叠部分的两段,然后取两段的极值做对比。为什么要取两段?因为二维数组里存的是从要查询的区间开始的、长为2的幂的区间的极值,我们查询的区间长度很有可能不是2的幂,因此需要分为两部分,这两部分一定会有重叠,这也隐含着ST表适用于那种有重叠部分也不影响结果的问题,如极值、求最大公约数等:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
        // 由于nums中元素个数最大为1000,因此2的10次方即可完全覆盖
        int exp = log(nums.size()) / log(2);
        vector<vector<pair<int, int>>> st(nums.size(), vector<pair<int, int>>(exp + 1, {-1, -1}));
        for (int i = 0; i < st.size(); ++i)
        {
            // 初始化,从i开始,长为2的0次方(即1)的极值为它自己
            st[i][0] = {nums[i], i};
        }

        // j最大为9,我们只会用到2的9次方的值,因为查找时我们会把区间分为两段
        for (int j = 1; j < st[0].size(); ++j)
        {
            // 循环条件部分保证了不会索引超出
            for (int i = 0; i + (1 << (j - 1)) < nums.size(); ++i)
            {
                // st[i][j - 1]是2的j-1次方个数字,为第一段
                // st[i + (1 << (j - 1))][j - 1]也是2的j-1次方个数字,从第一段的后面一个位置开始
                // 注意st[i + (1 << (j - 1))][j - 1]中的i + (1 << (j - 1))部分是循环条件
                st[i][j] = max(st[i][j - 1], st[i + (1 << (j - 1))][j - 1]);
            }
        }

        return build(0, nums.size() - 1, nums, st);
    }

private:
    TreeNode *build(int left, int right, vector<int> &nums, vector<vector<pair<int, int>>> &st)
    {
        if (left > right)
        {
            return nullptr;
        }

        // 以2为底,范围内数字个数的对数,将结果再去掉小数,这是最大的两段长度
        int num = log(right - left + 1) / log(2);
        // 两段会有交叉,如果left为0,right为5,则num为2
        // 此时会取从0开始的4个数字的极值和从1开始的4个数字的极值两者更大值
        const pair<int, int> &target = max(st[left][num], st[right - (1 << num) + 1][num]);
        int maxValue = target.first;
        int maxIndex = target.second;

        TreeNode *node = new TreeNode(maxValue);

        node->left = build(left, maxIndex - 1, nums, st);
        node->right = build(maxIndex + 1, right, nums, st);

        return node;
    }
};

此算法时间复杂度为O(nlogn),用于建表;空间复杂度为O(nlogn)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值