介绍
单调栈是什么?顾名思义,单调栈中的元素具有单调递增性或单调递减性,即栈中元素按顺序从小到大或者从大到小排列。从小到大单调递增称为单调递增栈,从大到小单调递减称为单调递减栈。
把数组的元素依次压入栈中,如果是单调递增栈,压入元素currentData
每次和栈顶元素peekData
进行比较,如果currentData < peekData
, 为了保证栈的单调递增性,栈顶元素须弹出,然后currentData
再不断和新的栈顶元素比较,直到currentData > peekData
或者栈为空。
如果单调递减栈,则条件反之,满足条件current > peekData
,栈顶元素弹出。
例子[1,5,6,3,4]
,模拟单调递增栈:
- 开始压入1,5,6,符合栈的单调递增性,栈内元素为{1,5,6}。
- 压入3,比栈顶元素6小,则6弹出,栈内元素为{1,5}。
- 3又比栈顶元素5小,则5弹出,栈内元素为{1}。
- 3比栈顶元素1大,则压入栈,栈内元素为{1,3}。
- 4比栈顶元素3大,则压入栈,栈内元素为{1,3,4}。
单调递增栈代码如下所示,单调递减栈,修改比较条件即可。
void monotonicStack() {
Stack<Integer> stack = new Stack();
int[] nums = {1,5,6,3,4};
for(int i = 0; i < nums.length; i++) {
while(!stack.isEmpty() && nums[i] < stack.peek()) {
stack.pop();
}
stack.push(nums[i]);
}
}
应用
单调栈可以用于确定元素的左右边界,找到数组中每个元素左右两边第一个比它大或者比它小的值。下面看leetcode的题目分析,单调栈如何应用的。
[84]、柱状图中最大的矩形
题目
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路解析
遍历所有柱子,计算每个柱子能占据的面积,比较确定占据的最大面积。
如果相邻柱子比当前柱子高即可以连成一片计算,如上面的题目示例,当前柱子高度为5,右边的柱子高度为6,则连成一片5*2=10。
所以先要找到每根柱子的左右边界,因为左右边界的范围内的柱子都比当前柱子高,以当前柱子高度作为矩形的高,左右边界相差的距离为矩形的宽,两者相乘即为当前柱子能占据的最大面积。按照此方法依次遍历所有柱子,计算出所有柱子占据的最大面积,最后比较得出最大值。
左边界和右边界即为比当前柱子高且距离最远的柱子,换个思路就是找到左右两边第一个比当前柱子小的,其相邻位置就是左右边界。
如下图所示,确定hegihts[3]
的左右边界的方法,就是找到hegihts[1]
和hegihts[6]
这两个离hegihts[3]
最近且小于它的柱子。确定左边界为hegihts[2]
右边界为hhegihts[5]
,从而计算出hegihts[3]
能占据矩形面积。
下面代码使用单调递增栈,用于确定左右边界,如果当前压入栈的柱子a高度小于栈顶的柱子b,说明当前压入的柱子a是右边第一个小于栈顶b的柱子,根据单调递增性,在栈顶下面的柱子c就是左边第一个小于栈顶b的柱子,两者的相邻位置即为栈顶柱子c的左右边界。
但是还有两种特殊情况
- 栈为空没有左边界。
- 遍历完柱子,栈内还有元素。
可以使用哨兵元素解决这两种特殊情况,在左和右添加高度为0的柱子作为哨兵,左边哨兵保证左边界的存在,右边哨兵由于为0是最小值,可以保证栈内元素全部出栈。
代码如下所示:
public static int largestRectangleArea(int[] heights) {
int[] copyHeights = new int[heights.length + 2];
System.arraycopy(heights, 0, copyHeights, 1, heights.length);
Deque<Integer> stack = new ArrayDeque<>(heights.length+2);
int maxArea = 0;
for (int i = 0; i < copyHeights.length; i++) {
while (!stack.isEmpty() && copyHeights[i] < copyHeights[stack.peek()]) {
int h = copyHeights[stack.pop()];
int left = stack.peek()+1;
int right = i -1;
maxArea = Math.max((right - left + 1) * copyHeights[i], maxArea);
}
stack.push(i);
}
return maxArea;
}
[739]、每日温度
题目
请根据每日气温列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。
例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。
提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。
思路解析
遍历数组中的每个温度,在右边找到第一个比当前温度值高的温度,然后两者索引相减即为等待天数。
使用单调递减栈,当前栈顶温度值如果小于于压入栈的温度值,当前压入栈的温度值即为第一个大于栈顶的温度值,把压入栈的索引-栈顶的索引=等待天数,然后栈顶温度出栈。
代码如下:
public int[] dailyTemperatures(int[] temperatures) {
ArrayDeque<Integer> stack = new ArrayDeque<>(temperatures.length);
int[] results = new int[temperatures.length];
for (int i = 0; i < temperatures.length; i++) {
while (!stack.isEmpty() && temperatures[stack.peek()] < temperatures[i]) {
int current = stack.pop();
results[current] = i - current;
}
stack.push(i);
}
return results;
}
其他常见题目
42. 接雨水
496. 下一个更大元素
901. 股票价格跨度
多学多练就可以掌握单调栈的解题套路。
总结
-
单调栈是栈顶元素按照单调递增性或递减性的特殊栈。
-
单调递增栈确定数组中每个元素的左右两边第一个比它小的值,单调递减栈确定数组中每个元素的左右两边第一个比它大的值。
-
设当前压入元素为a,栈顶元素为b, 栈顶元素的下一个元素为c。如果a>b,是栈顶元素弹出,则a是右边第一个大于栈顶的元素,c为左边第一个大于栈顶的元素。