持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第30天,点击查看活动详情
题目链接:654. 最大二叉树
题目描述
给定一个不重复的整数数组 nums 。 最大二叉树 可以用下面的算法从 nums 递归地构建:
- 创建一个根节点,其值为
nums中的最大值。 - 递归地在最大值 左边 的 子数组前缀上 构建左子树。
- 递归地在最大值 右边 的 子数组后缀上 构建右子树。
返回 nums 构建的 最大二叉树 。
提示:
1 <= nums.length <= 10000 <= nums[i] <= 1000nums中的所有整数 互不相同
示例 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]
整理题意
题目给定一个整数数组 nums,且数组中的元素是互不相同的,选择数组中的最大元素作为分界线,该最大元素为根节点,然后以该最大元素划分的左边元素为左子树,右边的元素为右子树,然后不断递归这个过程构造一颗最大二叉树。
解题思路分析
递归构造
我们可以直接根据题意模拟这个递归过程即可。
单调栈
我们还可以维护一个小顶堆的栈来实现构造这颗最大二叉树,这是因为对于数组中两个元素来说,下标较小的元素一定在下标较大的左边,也就是判断两个元素的左右关系;同时又可以根据元素大小关系来划分左右子树的关系,也就是判断两个元素的上下关系。又因为最大元素作为根节点,那么构造完成后根节点一定在栈底。
具体实现
递归构造
- 首先确定递归的边界:当数组中的元素为
0的时候停止递归; - 遍历数组寻找数组中的最大值;
- 以当前数组中的最大值作为分界线,取出左边和右边的元素然后分别递归构造最大二叉树;
- 将返回的左子树根节点连接到当前节点的左子树上,同理连接右子树根节点;
- 返回当前构造好的根节点即可。
单调栈
- 从左到右遍历数组中每一个元素;
- 对于当前元素
nums[i]来说:- 如果栈为空或者栈顶元素大于当前元素
nums[i],我们可以直接将当前元素压入栈,同时如果栈不为空,需要将栈顶元素的的右子树更新为当前元素nums[i]; - 如果栈顶元素小于当前元素
nums[i],我们将当前元素nums[i]的左子树更新为栈顶元素,然后弹出栈顶元素,重复这个操作,直至栈为空或栈顶元素大于当前元素nums[i],然后如果栈不为空,更新栈顶元素的右子树为当前元素nums[i]。
- 如果栈为空或者栈顶元素大于当前元素
- 重复步骤
1和步骤2直至遍历完数组,然后返回栈底元素即可。
复杂度分析
- 时间复杂度:
- 使用递归构造方法的时间复杂度为 ,其中 是数组 的长度。在最坏的情况下,数组严格递增或递减,需要递归 层,第 层需要遍历 个元素以找出最大值,总时间复杂度为 。
- 使用单调栈方法的时间复杂度为 ,其中 是数组 的长度。单调栈求解左右边界和构造树均需要 的时间。
- 空间复杂度:
- 使用递归构造方法的空间复杂度为 ,即为最坏情况下需要使用的栈空间。
- 使用单调栈方法的空间复杂度为 ,即为栈需要使用的空间。
代码实现
递归构造
/**
* 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) {
int n = nums.size();
// 处理边界情况
if(n == 0) return nullptr;
// pos记录最大值下标,m记录最大值
int pos = 0, m = nums[0];
// 找到 nums 数组中的最大值
for(int i = 1; i < n; i++){
if(nums[i] > m){
m = nums[i];
pos = i;
}
}
// 左子树元素数组 l,右子树元素数组 r
vector<int> l, r;
l.clear(), r.clear();
for(int i = 0; i < pos; i++){
l.emplace_back(nums[i]);
}
for(int i = pos + 1; i < n; i++){
r.emplace_back(nums[i]);
}
// 当前节点 now
TreeNode* now = new TreeNode(m);
// 递归构造左子树和右子树
now->left = constructMaximumBinaryTree(l);
now->right = constructMaximumBinaryTree(r);
return now;
}
};
单调栈
/**
* 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<TreeNode*> s; while(s.size()) s.pop();
int n = nums.size();
for(int i = 0; i < n; i++){
// 创建节点
TreeNode* now = new TreeNode(nums[i]);
// 当栈为空 或者 栈顶元素大于当前待插入元素时
if(s.empty() || s.top()->val > now->val){
// 将栈顶元素的右子树赋值为当前元素
if(s.size()) s.top()->right = now;
// 入栈
s.push(now);
}
else{
// 当栈不为空且栈顶元素小于当前待插入元素时
while(!s.empty() && s.top()->val < now->val){
// 不断更新当前待插入元素的左子树
now->left = s.top();
s.pop();
}
// 如果栈不为空,更新栈顶元素的右子树
if(!s.empty()){
s.top()->right = now;
}
s.push(now);
}
}
TreeNode* ans;
// 取出栈底的元素为根节点
while(s.size()){
ans = s.top();
s.pop();
}
return ans;
}
};
总结
- 由于题目给定的数据范围较小( 以内),所以可以采用较为暴力的递归构造,每次遍历数组寻找最大值作为根节点,然后递归构造左子树和右子树。
- 当题目数据范围较大时( 以内)就只能使用单调栈的方法进行解题了,在使用单调栈方法实现的时候需要注意判断栈中元素是否为空的情况,避免运行错误。
- 该题的难点在于单调栈的方法在思想上没有递归构造那么直观,很难想到单调栈的方法进行解题。可以将本题抽象化为找最近元素的问题,那么就很容易想到使用单调栈的方法了。
- 测试结果:
根据测试结果可以很直观的看到单调栈的方法在时间和空间的复杂度方面都优于递归构造的方法。
结束语
你的担忧往往是因为想得太多而做的太少。与其被困在想象的恐惧中,不如采取行动,打破心中的恐惧。千里之行,始于足下,当你真正行动起来就会发现,你所认为的难题并没有想象中那样艰难。新的一天,加油!