栈(stack)是一种简单的数据结构,数据先进后出。单调栈实际上就是栈,利用一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)
首先考虑一个问题:
给定一个数组,返回一个等长的数组,对应索引位置存储着比当前元素下一个更大元素,如果不存在这样的数,就存-1。
eg:给定数组[3,1,3,4,2],则返回数组[4,3,4,-1,-1] 说明:第一个数3后面比它大的数4,第二个数1后面比它大的是3,第三个数3后面比它大的是4,第四个数4后面没有更大的数了,存-1,最后一个数2后面没有更大的数了,存-1。
这样的题目,使用暴力法就可以解决,对每个元素后面都进行扫面,找到第一个更大的元素就行了,时间复杂度为O(n^2)。
换一种方法,我们可以这样考虑,结合上述例子,首先从数组的尾部开始遍历并找到下一个更大元素,对于末尾的数2,后面没有数据了,所以对应位置应该填-1,将末尾元素2入栈。接着遍历到前一个元素4,此时比较该元素和栈顶元素2的大小,比较小,出栈,此时栈为空,所以对应位置填-1,将元素4入栈;继续遍历元素3,此时栈顶元素为4,比较大,不用出栈,对应位置应该填4,将元素3入栈;继续遍历元素1,栈顶元素为3,比较大,不用出栈,对应位置应该填3,将元素1入栈;继续遍历元素3,此时栈内从栈顶开始有数据(1,3,4),栈顶元素为1,比较小,出栈,然后栈顶元素为3,不符合更大的要求,继续出栈,直到直到更大的数,此时栈顶是4,比较大,不用出栈,对应位置填4,将元素3入栈,至此,遍历结束,得到结果数组 [4,3,4,-1,-1]。
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> ans(nums.size()); // 存放结果的数组
stack<int> s;
for (int i = nums.size() - 1; i >= 0; i--) {
// 从数组末尾开始入栈,这样后面比较栈顶元素的时候最先遇到第一个比较大的数
while (!s.empty() && s.top() <= nums[i]) { // 判断大小
s.pop(); // 将比当前数小的出栈,直到找到更大的数,或者找不到
}
ans[i] = s.empty() ? -1 : s.top(); // 该元素后面第一个较大的数
s.push(nums[i]); // 进栈,之后接受大小比较
}
return ans;
}
根据每日气温列表,重新生成一个列表。对应位置的输出为:到下一次升温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。这道题和上述问题一模一样,至少存储的不再是更大数值本身,而是数组下标。这里不再累述,直接看代码。
vector<int> nextGreaterElement(vector<int>& nums) {
vector<int> ans(nums.size());
stack<int> s;
for (int i = nums.size() - 1; i >= 0; i--) {
while (!s.empty() && num[s.top()] <= nums[i]) {
s.pop();
ans[i] = s.empty() ? 0 : s.top()-i;
s.push(i);
}
return ans;
}
最后一个扩展应用是 求柱状图中最大矩形面积:
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例: 输入: [2,1,5,6,2,3] 输出: 10
本题使用单调栈的思想,遍历一遍数组就可以得到最大矩形面积。思路是对于每一个柱子,我们求出能完全包含它的的矩形的面积,然后对所有柱子得到的面积求出其中的最大值即可。
那么对于某一个柱子我们如何求能完全包含它的矩形面积,比如对于第五个柱子,高度是已知的,我们只要求得最大的宽度,就可以得到包含该柱子的最大矩形面积。如何求最大宽度?宽度可以看出分左边界和有边界,都满足条件,就是到第一个比自己矮的柱子为止。
从头开始遍历,只要高度是递增的,就一直将下标入栈,表明目前遍历的柱子右边界还没到,一旦遍历到矮一点的柱子,我们就出栈并计算出栈柱子的矩形面积,(说明栈内有柱子右边界到了,宽度不能再增加了),直到当前这个矮柱子高度不比栈内的柱子低(说明栈内的柱子右边界还可能继续增加),将该位置的下标入栈。最后继续遍历重复上述操作。
宽度的左边界如何确定呢?左边界就是该位置向前查找直到第一个比自己矮的柱子位置,我们计算出栈的柱子矩形面积时,下一个栈顶柱子就是第一个比自己矮的柱子(因为栈内存放的元素始终是递增的),所以左边界就是下一个栈顶元素的下标,宽度就是当前遇到的这个矮柱子的下标减去栈顶元素的下标再减去1,即 i-stack.top()-1;若是栈为空了,说明该柱子左边界无限制,宽度从头开始,即为i 。
这里需要注意一点,因为我们是在出栈的时候计算矩形面积,要保证所有元素都出栈,所以我们在给定的数组尾部添加一个较小的值,保证原数组中的每一个柱子的矩形面积都能被计算。
结合代码很好理解,如下:
int RectangleArea(vector<int>& heights) {
heights.push_back(-1); //这里在尾部添加一个-1,为了使栈内元素都能出栈并计算矩形面积
stack<int> st;
int maxArea = 0;
for(int i = 0;i<heights.size();i++)
{
while(!st.empty() && heights[i]<heights[st.top()])
{
int top= st.top();
st.pop();
//栈顶元素出栈,所以下面计算宽度时,栈顶元素就是当前出栈柱子的左边第一个较小的柱子下标
maxArea = max(maxArea,heights[top]*(st.empty()? i:(i - st.top() -1)));
//栈为空,则说明当前柱子左边边界无限制,宽度为i
}
st.push(i);
}
return maxArea;
}