单调栈学习与应用
栈的应用,单调栈就是保持栈内元素有序,需要我们自己维持顺序,从栈顶到栈底是从小到大和从大到小两种情况。
- 若要找第一个小于的,则应该从大到小
- 若要找第一个大于的,应该从小到大
【LeetCode】84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10
示例 2:
输入: heights = [2,4]
输出: 4
提示:
1 <= heights.length <=105
0 <= heights[i] <= 104
思路
对于每一个柱子,想要合并成更大的矩形,只有两侧的柱子的高度大于等于当前这个柱子才能够合并,对于暴力解法来说,只要找到每一个柱子左侧第一个小于它高度的柱子,右侧第一个小于它高度的柱子,就能求出每一个柱子的合并最大矩形,遍历每个柱子就可以得到最大值!
若使用单调栈:
-
当前柱子高度大于栈顶柱子的高度
- 入栈
-
当前柱子高度等于栈顶柱子的高度
1和2代表两种不同的思路:
- 弹出栈顶柱子,入栈当前柱子(代表对于相同高度的柱子不重复计算)
- 直接入栈(代表遍历每一个柱子的合并面积,取最大值)
-
当前柱子高度小于栈顶柱子的高度 矩形面积为 height * width
- 弹出栈顶柱子,以当前栈顶柱子高度进行合并 下标为left
- 不弹出新栈顶柱子,以新栈顶柱子为左侧第一个小于要合并的高度的柱子 下标为cur
- 以当前遍历的柱子为右侧第一个小于要合并的高度的柱子 下标为right
- width = right - left - 1
- 计算面积并选取面积最大值,如果当前遍历柱子高度仍小于新栈顶柱子高度,则继续重复1-5步骤
- 将当前遍历的柱子入栈
因为单调栈是有序的,当找到第一个小于栈顶的,则栈顶的下一个柱子一定是小于栈顶的(如果是相等的也看作是小于的),所以就可以用栈顶柱子,栈顶下一个柱子,当前遍历柱子计算出合并面积!
例子
heights = [2,1,5,6,2,3]
添加哨兵后heights = [0,2,1,5,6,2,3,0]
heights[0]=0,入栈s=[0]
heights[1]=2,入栈s=[0,1]
heights[2]=1 < 2,pop出栈顶元素1,s=[0],可以确定heights[1]=2所能勾勒出的最大矩形面积,左边界为新栈顶元素0,右边界就是当前下标i=2,宽为2-0-1=1,高度为heights[1]=2,ans=max(0,2)=2。1>heights[0]=0,所以不继续pop,入栈s=[0,2]
heights[3]=5,入栈s=[0,2,3]
heights[4]=6,入栈s=[0,2,3,4]
heights[5]=2 < heights[4]=6,pop出栈顶元素4,s=[0,2,3],可以确定heights[4]=6所能勾勒出的最大矩形面积,左边界为新栈顶元素3,右边界就是当前下标i=5,宽为5-3-1=1,高度为heights[4]=6,ans=max(2,6)=6
< heights[3]=5,pop出栈顶元素3,s=[0,2],可以确定heights[3]=5所能勾勒出的最大矩形面积,左边界为新栈顶元素2,右边界就是当前下标i=5,宽为5-2-1=2,高度为heights[3]=5,ans=max(6,10)=10。2>heights[2]=1,所以不继续pop,入栈s=[0,2,5]
heights[6]=3,入栈s=[0,2,5,6]
heights[7]=0 < heights[6]=3,pop出栈顶元素6,s=[0,2,5],可以确定heights[6]=3所能勾勒出的最大矩形面积,左边界为新栈顶元素5,右边界就是当前下标i=7,宽为7-5-1=1,高度为heights[6]=3,ans=max(10,3)=10
< heights[5]=2,pop出栈顶元素5,s=[0,2],可以确定heights[5]=2所能勾勒出的最大矩形面积,左边界为新栈顶元素2,右边界就是当前下标i=7,宽为7-2-1=4,高度为heights[5]=2,ans=max(10,8)=10
< heights[2]=1,pop出栈顶元素2,s=[0],可以确定heights[2]=1所能勾勒出的最大矩形面积,左边界为新栈顶元素0,右边界就是当前下标i=7,宽为7-0-1=6,高度为heights[2]=1,ans=max(10,6)=10。不继续pop,入栈s=[0,7]
遍历结束,ans=10
tips
若所有柱子全部都是单调递增的,则找不到左右侧第一个小于的,则无法计算合并面积,怎么办?
例如:5 5 6
在开头和末尾加一个高度为0的哨兵即可,这样保证了栈一定不为空,同时,能保证一定能计算所有柱子的合并面积!
前后加0后:0 5 5 6 0,保证每个柱子都找得到比它小的
代码
弹出栈顶柱子,入栈当前柱子(代表对于相同高度的柱子不重复计算)
class Solution {
public int largestRectangleArea(int[] heights) {
int[] newHeights = new int[heights.length + 2];
for(int i = 0; i < heights.length; i++) {
newHeights[i + 1] = heights[i];
}
heights = newHeights;
Stack<Integer> stack = new Stack<>();
stack.push(0);
int res = 0;
for(int i = 1; i < heights.length; i++) {
if(heights[i] > heights[stack.peek()]) {
stack.push(i);
}else if(heights[i] == heights[stack.peek()]){
stack.pop();
stack.push(i);
}else {
while(heights[i] < heights[stack.peek()]) {
int cur = stack.pop();
int left = stack.peek();
int right = i;
int height = heights[cur];
int width = right -left - 1;
res = Math.max(res, height * width);
}
stack.push(i);
}
}
return res;
}
}
直接入栈(代表遍历每一个柱子的合并面积,取最大值)
class Solution {
public int largestRectangleArea(int[] heights) {
int[] newHeights = new int[heights.length + 2];
for(int i = 0; i < heights.length; i++) {
newHeights[i + 1] = heights[i];
}
heights = newHeights;
Stack<Integer> stack = new Stack<>();
stack.push(0);
int res = 0;
for(int i = 1; i < heights.length; i++) {
if(heights[i] > heights[stack.peek()]) {
stack.push(i);
}else if(heights[i] == heights[stack.peek()]){
stack.push(i);
}else {
while(heights[i] < heights[stack.peek()]) {
int cur = stack.pop();
int left = stack.peek();
int right = i;
int height = heights[cur];
int width = right -left - 1;
res = Math.max(res, height * width);
}
stack.push(i);
}
}
return res;
}
}
化简:
class Solution {
public int largestRectangleArea(int[] heights) {
int[] newHeights = new int[heights.length + 2];
for(int i = 0; i < heights.length; i++) {
newHeights[i + 1] = heights[i];
}
heights = newHeights;
Stack<Integer> stack = new Stack<>();
stack.push(0);
int res = 0;
for(int i = 1; i < heights.length; i++) {
while(heights[i] < heights[stack.peek()]) {
int cur = stack.pop();
int left = stack.peek();
int right = i;
int height = heights[cur];
int width = right -left - 1;
res = Math.max(res, height * width);
}
stack.push(i);
}
return res;
}
}
【LeetCode】42. 接雨水
给定 n
个非负整数表示每个宽度为 1
的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
思路
使用单调栈,就是出现有坑的时候就能存储水,怎样判断出现坑?当出现第一个大于栈顶柱子高度的柱子时,出现坑。那么单调栈的顺序从栈顶到栈底就应该是从小到大。单调栈的方法是以行计算雨水量。
-
当前柱子高度小于栈顶柱子的高度
- 入栈
-
当前柱子高度等于栈顶柱子的高度
1和2代表两种不同的思路:
-
直接入栈(代表对于相同高度的柱子分别成坑计算,比如有两个相同高度的柱子相邻和下图的情况)
1.当①和②中间的高度为0的柱子被弹出后②作为栈顶,①为左侧大于②的柱子(也就是栈顶下一个柱子),②右侧的柱子为右侧大于②的柱子(也就是当前遍历的柱子),形成了坑,其接的雨水为1个宽度
2.当②弹出后,①作为栈顶,①左侧柱子作为左侧大于①的柱子(也就是栈顶下一个柱子),②右侧的柱子为右侧大于①的柱子(也就是当前遍历的柱子),形成了坑,其接的雨水是两个宽度
-
弹出栈顶柱子,入栈当前柱子(直接计算有相同高度柱子的承接雨水最大值,相当于一步到位)
-
-
当前柱子高度大于栈顶柱子的高度 雨水面积为 h * width
- 弹出栈顶柱子,以当前栈顶柱子高度进行合并 下标为left
- 不弹出新栈顶柱子,以新栈顶柱子为左侧第一个小于要合并的高度的柱子 下标为cur
- 以当前遍历的柱子为右侧第一个小于要合并的高度的柱子 下标为right
- width = right - left - 1
- h = Math.min(height[stack.peek()], height[right]) - height[cur];
- 计算雨水并累加,如果当前遍历柱子高度仍大于新栈顶柱子高度,则继续重复1-5步骤
- 将当前遍历的柱子入栈
代码
直接入栈
class Solution {
public int trap(int[] height) {
int len = height.length;
Stack<Integer> stack = new Stack<>();
stack.push(0);
int sum = 0;
for(int i = 1; i < len; i++) {
if(height[i] < height[stack.peek()]) {
stack.push(i);
}else if(height[i] == height[stack.peek()]) {
stack.push(i);
}else {
while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
int cur = stack.pop();
if(!stack.isEmpty()) {
int right = i;
int left = stack.peek();
int h = Math.min(height[left], height[right]) - height[cur];
int width = right - left - 1;
sum += h * width;
}
}
stack.push(i);
}
}
return sum;
}
}
简化:
class Solution {
public int trap(int[] height) {
int len = height.length;
Stack<Integer> stack = new Stack<>();
stack.push(0);
int sum = 0;
for(int i = 1; i < len; i++) {
while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
int cur = stack.pop();
if(!stack.isEmpty()) {
int right = i;
int left = stack.peek();
int h = Math.min(height[left], height[right]) - height[cur];
int width = right - left - 1;
sum += h * width;
}
}
stack.push(i);
}
return sum;
}
}
弹出栈顶柱子,入栈当前柱子
class Solution {
public int trap(int[] height) {
int len = height.length;
Stack<Integer> stack = new Stack<>();
stack.push(0);
int sum = 0;
for(int i = 1; i < len; i++) {
if(height[i] < height[stack.peek()]) {
stack.push(i);
}else if(height[i] == height[stack.peek()]) {
stack.pop();
stack.push(i);
}else {
while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
int cur = stack.pop();
if(!stack.isEmpty()) {
int right = i;
int left = stack.peek();
int h = Math.min(height[left], height[right]) - height[cur];
int width = right - left - 1;
sum += h * width;
}
}
stack.push(i);
}
}
return sum;
}
}