单调栈
- 简介
- 例题:
- [496. 下一个更大的元素 I](https://leetcode.cn/problems/next-greater-element-i/)
- [503. 下一个更大的元素 II](https://leetcode.cn/problems/next-greater-element-ii/)
- [654. 最大二叉树](https://leetcode.cn/problems/maximum-binary-tree/)
- [581. 最短无序连续子数组](https://leetcode.cn/problems/shortest-unsorted-continuous-subarray/)
- [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/)
简介
单调栈是栈内元素满足单调性的栈,主要用来在O(n)时间复杂度下解决在数组中查找邻近的满足某种条件的元素的问题。
使用单调栈的核心是: 在将元素依次入栈的过程中,若即将入栈的元素会破坏单调性,则此元素就是栈内所有加入此元素后破坏栈内元素单调性的元素的解,此时依次更新栈顶元素的解并且弹出栈顶元素,直至加入当前元素后仍能保证栈的单调性为止。
它记录了一种多对一的对应关系(举例来说,向量[5,4,3,2,7]而言,5,4,3,2四个元素右侧第一个比它大的元素都是7; 向量[2,3,4,5,1]中,前四个元素右侧第一个比它小的元素都是1),当栈的单调性被破坏时,栈内导致单调性破坏的元素的解就是当前处理的位置。
依据要求可选用 {单调增、单调减}, {前向遍历、后向遍历} 二者两两组合。一般来说:
- 前向遍历单调递减栈: 查找元素右侧第一个比它大的元素
- 前向遍历单调递增栈: 查找元素右侧第一个比它小的元素
- 后向遍历单调递减栈: 查找元素左侧第一个比它大的元素
- 后向遍历单调递增栈: 查找元素左侧第一个比它小的元素
一般代码模板为:
std::stack<int> stk;
for (int i = 0; i < nums.size(); i++)
{
while (!stk.empty() && check(nums[stk.top()] ,nums[i] )) // 破坏单调性
{
updateResult(stk,nums[i]); // 找到当前元素的解
stk.pop();
}
stk.push(i);
}
例题:
496. 下一个更大的元素 I
查找nums1中每一个元素对应值在nums2中的下一个更大的元素,由于nums1是nums2的子集,此题可以分为两步:
- 找到nums2中每一个元素的下一个更大的元素 (前向单调递减栈直接套模板)
- 映射回nums1的结果(使用map映射)
题解如下:
inline bool check(const int& a, const int& b)
{
return a < b;
}
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> stk;
unordered_map<int,int> umap;
for(int i =0 ; i< nums2.size(); i++)
{
while(!stk.empty() && check(nums2[stk.top()],nums2[i]))
{
umap[nums2[stk.top()]] = nums2[i];
stk.pop();
}
stk.push(i);
}
vector<int> res(nums1.size(),-1);
for(int i=0; i< nums1.size();i++)
{
if(umap.count(nums1[i]) !=0)
res[i] = umap[nums1[i]];
}
return res;
}
503. 下一个更大的元素 II
在循环数组中查找右侧下一个更大的元素,循环可以使用第二次遍历来模拟。按题意将结果集初始化为全-1,遍历时截至条件使用2*size(),然后使用 index % size()来取值,套用模板即可:
vector<int> nextGreaterElements(vector<int>& nums) {
int Len2 = nums.size()*2;
vector<int> stack;
vector<int> res(nums.size(),-1);
for(int i=0; i<Len2; i++)
{
while(!stack.empty() && nums[stack.back()] < nums[i%nums.size()])
{
res[stack.back()] =nums[i%nums.size()] ;
stack.pop_back();
}
stack.push_back(i%nums.size());
}
return res;
}
654. 最大二叉树
分析:
- 对任意一个节点X来说,所有位于它数组位置左侧并且小于它的值位于它的左子树上,并且其中的最大值Y是左子树树根。所以对于任意节点Y,找到数组位置右侧的第一个比它大的节点X,则Y是X的左孩子。因此可以前向遍历构造单调递增栈完成。
- 所有位于X右侧的小于X值的节点,必然位于它的右子树上,只需要在右子树上找到合适的位置安顿下来即可,安顿的逻辑是,如果当栈顶节点右孩子的值比当前节点要大,则深入栈顶节点的右子树继续从右子树中查找,直到右孩子为空或者右孩子的值小于当前节点,此时当前节点作为右子树树根,原右子树树根作为当前节点的左孩子。(见函数 adjustRight)
- 至于树根则必然是整个数组的全局最大值,在遍历的过程中记录下最大值即可找到全树的树根。
代码如下:
void adjustRight(TreeNode* root, TreeNode* current)
{
if(!root) return;
TreeNode* tmp = root;
while(tmp->right && tmp->right->val >current->val)
tmp = tmp->right;
current->left = tmp->right;
tmp->right = current;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
stack<TreeNode*> nodeStack;
TreeNode* tmpNode = nullptr;
TreeNode* root = nullptr;
for (auto& x: nums) {
tmpNode = new TreeNode(x);
while(!nodeStack.empty() && nodeStack.top()->val < x)
{
tmpNode->left = nodeStack.top();
nodeStack.pop();
}
if(!nodeStack.empty() && nodeStack.top()->val > x)
adjustRight(nodeStack.top(),tmpNode);
nodeStack.push(tmpNode);
if(!root || root->val < tmpNode->val) root = tmpNode;
}
return root;
}
581. 最短无序连续子数组
依据题目,只需找出左侧第一个无序位置和右侧最后一个有序位置,二者中间便是乱序区间。找位置的题目都可以考虑单调栈。
找左侧第一个无序位置可以构造一个单调递增的栈,由于单调性,最后一个出栈的元素所在位置必然是左端点。
找右侧第一个有序位置可以构造一个单调递减的栈,由于单调性,最后一个出栈的元素所在位置必然是右端点。
代码如下(可优化空间还比较大):
int findUnsortedSubarray(vector<int>& nums) {
int lf=nums.size()-1,rt=0;
stack<int> stkIn;
stack<int> stkDe;
for(int i=0; i< nums.size(); i++)
{
while(!stkIn.empty() && nums[stkIn.top()] > nums[i])
{
lf =min(lf,stkIn.top());
stkIn.pop();
}
stkIn.push(i);
}
if(lf==nums.size()-1) return 0; // 全局有序无需找右端点了
for (int i = nums.size() - 1; i >= 0; i--)
{
while (!stkDe.empty() && nums[stkDe.top()] < nums[i])
{
rt = max(rt, stkDe.top());
stkDe.pop();
}
stkDe.push(i);
}
return rt - lf +1;
}
42. 接雨水
能接雨水的条件是两头高中间低,于是可以利用单调递减栈, 破坏栈的单调性时说明找到局部凹陷处,可以接雨水了。不失一般性,凡是可以接雨水的区域必然满足栈内至少两个元素,当前栈顶是凹陷处,出栈之后的新栈顶为左边界,入栈之前的当前位置即为右边界。由于需要使用左边界,所以此处计算雨水需要在出栈之后计算,因此需要先记录下凹陷处的高bottom。于是雨水的高度为
左右边界中较小的一个减去底部高度,即 min(height[rt], height[lf]) - bottom
代码如下:
int trap(vector<int>& height)
{
int ans = 0;
int bottom= 0;
stack<int> stk;
for (int i = 0; i < height.size(); i++)
{
while (!stk.empty() && height[stk.top()] < height[i])
{
bottom = height[stk.top()];
stk.pop();
if (stk.empty()) break;
int h = min(height[i], height[stk.top()]) - bottom ;
ans += (i - stk.top() - 1) * h;
}
stk.push(i);
}
return ans;
}