单调栈问题汇总
单调栈(Monotone Stack)
栈的应用中有一类问题称为单调栈(Monotone Stack)问题,可以巧妙的将某些问题的时间复杂度降到「O(n)级别」。那么什么是单调栈呢?
所谓单调栈,就是保持栈中的元素是单调的。假设把数组 [2 1 4 6 5]依次入栈,并保持栈的单调递增性,如下:
- 元素2入栈,此时栈中元素为[2]
- 元素1入栈,由于此时1小于栈顶元素2,把1入栈的话就不满足单调递增性了,于是先把栈顶元素2弹出,再让元素1入栈,此时栈中元素为[1]
- 元素4入栈,由于此时4大于栈顶元素1,可以满足递增性,故入栈,此时栈中元素为[1,4]
- 元素6入栈,同上,此时栈中元素为[1,4,6]
- 元素5入栈,同第2步,在入栈前先把栈顶元素6弹出,故此时栈中元素为[1,4,5]
由于栈中元素(从栈底至栈顶)保持单调递增性,因此,有这样一个性质:
假设当前元素为a,栈顶元素(若栈非空)就是元素a左侧第一个小于a的元素
同样的,如果维护一个单调递减栈,那么就有:
假设当前元素为a,栈顶元素(若栈非空)就是元素a左侧第一个大于a的元素
下面就来看一下单调栈的应用。
42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
分析:本题是单调栈的典型应用。首先,我们考虑该选取单调递减栈还是单调递增栈?
由于要接到雨水,显然,必须要形成“凹”的形状,由此可以确定,应当是选取递减栈——按递减的序列把高度存进去(递减指的是从栈底至栈顶递减),一旦发现当前的高度大于栈顶元素了,说明形成了凹槽。但是计算凹槽的面积,除了高度,还需要知道宽度,因此,我们应该在栈中存放下标而非直接存放高度。
class Solution {
public int trap(int[] height) {
int n = height.length;
int total = 0; // 能接到的雨水总量
Stack<Integer> s = new Stack<>(); // 存放数组下标,而非数组元素
int i = 0;
while(i < n) {
// 维护一个单调递减栈
if(s.isEmpty() || height[i] < height[s.peek()]) {
s.push(i);
i++;
}else {
int bottom = s.pop();
if(s.isEmpty()) continue; // 关键
int w = i - s.peek() - 1;
int h = Math.min(height[s.peek()], height[i]) - height[bottom];
total += w * h;
}
}
return total;
}
}
84. 柱状图中最大的矩形
方法1:暴力法O(n^2)
算法思路:
从最基础的思路出发,已知矩形面积的计算公式为:高度 × 宽度。就本题而言,每个小矩形的高度是确定,我们可以固定一个小矩形i,以heights[i]
为高度,以位置 i 为中心向左右两侧扩散,使得扩展到的柱子的高度均不小于 h,直到到达边界、或者不能再向外延伸了。
换句话说,我们需要找到左右两侧最近的高度小于 h 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 i 能够扩展到的最远范围。
这种做法的时间复杂度是O(n^2),因为遍历数组需要O(n),固定位置i向两侧延伸时最大也需要O(n),即两层for循环。
class Solution {
/*
暴力解法
时间复杂度:O(n^2)
空间复杂度:O(1)
*/
public int largestRectangleArea(int[] heights) {
int maxArea = 0;
for(int i = 0; i < heights.length; i++) {
int h = heights[i];
int left = i, right = i;
while(left >= 0 && heights[left] >= h) left--;
while(right < heights.length && heights[right] >= h) right++;
int w = right - left - 1;
maxArea = Math.max(maxArea, w * h);
}
return maxArea;
}
}
方法2:单调栈O(n)
class Solution {
public int largestRectangleArea(int[] heights) {
// 预处理:添加哨兵
int n = heights.length;
int[] temp = new int[n + 1];
for(int i = 0; i < n; i++) {
temp[i] = heights[i];
}
temp[n] = 0;// 哨兵
heights = temp;
// 正式处理
Stack<Integer> s = new Stack<>();
int maxArea = 0, i = 0;
while(i < heights.length) {
if(s.isEmpty() || heights[i] >= heights[s.peek()]) {
s.push(i);
i++;
}else{
int t = s.pop();
if(s.isEmpty()) {
maxArea = Math.max(maxArea, i * heights[t]);
}else {
maxArea = Math.max(maxArea, (i - s.peek() - 1) * heights[t]);
}
}
}
return maxArea;
}
}
单调栈:不使用Stack,使用Deque
使用双端队列来模拟栈,速度更快一些。因为在Java中,Stack其实不推荐使用的。
class Solution {
public int largestRectangleArea(int[] heights) {
// 预处理:添加哨兵
int n = heights.length;
int[] temp = new int[n + 1];
for(int i = 0; i < n; i++) {
temp[i] = heights[i];
}
temp[n] = 0;// 哨兵
heights = temp;
// 正式处理
Deque<Integer> s = new LinkedList<>();
int maxArea = 0, i = 0;
while(i < heights.length) {
if(s.isEmpty() || heights[i] >= heights[s.peekLast()]) {
s.add(i);
i++;
}else{
int t = s.pollLast();
if(s.isEmpty()) {
maxArea = Math.max(maxArea, i * heights[t]);
}else {
maxArea = Math.max(maxArea, (i - s.peekLast() - 1) * heights[t]);
}
}
}
return maxArea;
}
}
方法3:暴力优化(本题最优解)
class Solution {
public int largestRectangleArea(int[] heights) {
if(heights.length == 0) return 0;
int maxArea = 0;
int n = heights.length;
/*
left[i] 表示位置i左侧第一个小于heights[i]的位置
right[i] 表示位置i右侧第一个小于heights[i]的位置
*/
int[] left = new int[n];
int[] right = new int[n];
left[0] = -1;
for(int i = 1; i < n; i++) {
int k = i-1;
while(k >= 0 && heights[k] >= heights[i]) {
// k--;
k = left[k];
}
left[i] = k;
}
right[n-1] = n;
for(int i = n-2; i >= 0; i--) {
int k = i+1;
while(k < n && heights[k] >= heights[i]) {
// k++;
k = right[k];
}
right[i] = k;
}
/*
计算面积
对于高度为heights[i]的柱子,以其为中心可以形成的最大矩形面积等于
heights[i] × (right[i] - left[i] - 1)
*/
for(int i = 0; i < n; i++) {
int currArea = heights[i] * (right[i] - left[i] - 1);
maxArea = Math.max(maxArea, currArea);
}
return maxArea;
}
}