单调栈,顾名思义,就是栈中的元素必须保持一定的顺序,可以递增或递减,如下图演示一个单调栈,先放入一个元素5,再放入一个元素4,现在我们希望栈中的元素是由底到顶逐级递减的,因此4比5小可以直接放入,同理放入3、2、1
接下来如果想再放入一个元素3,3都比2和1大,那就需要先把2和1删除后再放入3,这样就满足逐级递减的规则了,如下图:
运用单调栈可以解决什么样的问题呢?看下面这道力扣题:
数组中的每个数字代表柱子的高度。显然,下雨后如果遇到了两根凹下去的柱子,那么它们的中间就可以接到一定量的雨水,但是最右边的柱子就不能接到雨水了,因为它右边没有更多的柱子挡住雨水,同样的左边的也不行,如题图
解题思路:首先我们可以维护一个单调栈,然后将柱子也就是一个个元素加入到单调栈中,比入我们可以先加入一个高度为1的柱子,再加入一个高度为0的柱子,这目前是没有违反单调规则的。接下来如果要加入一个高度为2的柱子就不行,它要把之前比2小的这些柱子弹出栈,这意味这着当我们加入一个新元素以后发现需要弹出元素了,就相当于遇到了一个凹陷下去的位置,那这个时候就可以计算雨水的容量了。
容量计算:我们可以用 left 表示左边的柱子,right 表示右边的柱子,它们也有各自的索引位置,假设左边柱子的索引为 i ,右边柱子的索引为 j ,要计算雨水的容量需要先找凹槽的宽度,显然下图例子的宽度就是 j - i - 1,高度就是left 与right 找到一个更小的就是凹槽的高度(木桶效应),然后宽 x 高就是这块雨水的容量,然后把所有凹陷下去地方雨水容量累加起来就是总容量
注意点1:在最左边放入高度为0的柱子后,再放入高度为1的柱子,虽然这个时候违反了单调递减的规则需要弹出元素,但我们是不用计算雨水的容量的(挡不住水)
注意点2:并不是弹出几根柱子就要计算几次水的容量,当要弹出柱子左边没有其他柱子时它也相当于拦不住水,只要弹出即可,如下图标注2柱子左边为0高度的柱子弹出后计算一次容量,标注为1的柱子同样也需要弹出,但不必计算雨水容量
实现思路:
1.原始方法 trap 接受一个参数即柱子数组,返回结果为雨水容量
2.往单调栈里存入柱子数据的时候,不光是要柱子高度,还需要柱子的索引位置,就需要创建一个新的类型 Data 包含当前放入单调栈的柱子高度以及柱子索引位置
3.创建一个单调栈,类型就是 Data
4.遍历数组
5.将第 i 根柱子的高度和柱子的索引 i 封装成Data传到栈中(要放入的柱子,命名为right)
6.在柱子放入之前要做一个检查,检查要多次,遇到多个比它矮的柱子都要弹出,检查的条件是栈不为空,且栈顶的柱子的高度小于要放入的柱子高度,此时应该将栈顶的柱子移除
7.检查代码中要求只在有能拦得住水的左柱子的情况下才计算水的容量
8.将右边的柱子放入栈,再次循环遍历,返回结果 sum
代码实现:
/**
* <h3>接雨水 - 单调栈</h3>
*/
public class TrappingRainWaterLeetcode42 {
/*
1.维护一个单调栈
2.当加入一个新元素时,如果发现需要弹出元素,表示遇到了一个凹陷的位置,此时应该计算雨水的容量
*/
static int trap(int[] heights) { //原始方法接受一个参数即柱子数组,返回结果为雨水容量
LinkedList<Data> stack = new LinkedList<>(); //创建一个单调栈
int sum = 0;
for (int i = 0; i < heights.length; i++) { //遍历数组
Data right = new Data(heights[i], i); //将第i根柱子的高度和柱子的索引i封装成Data传到栈中(要放入的柱子,命名为right)
//在柱子放入之前要做一个检查,检查要多次,遇到多个比它矮的柱子都要弹出
while (!stack.isEmpty() && stack.peek().height < right.height) {
//栈不为空,且栈顶的柱子的高度小于要放入的柱子高度,应该将栈顶的柱子移除
Data pop = stack.pop(); //移除
Data left = stack.peek(); //重新拿到栈顶元素,找到左侧的柱子
if (left != null) { // 只在有能拦得住水的左柱子的情况下才计算水的容量
int width = right.i - left.i - 1; //通过索引计算宽度
int height = Math.min(left.height, right.height) - pop.height;
//高度:在栈顶柱子(左柱子)和右柱子中选择较小者,然后减去弹出柱子的高度才是中间区域的高度
sum += width * height;
}
//栈被移空或者遇到了更高的柱子就退出循环
}
stack.push(right); //将右边的柱子放入栈
// System.out.println(stack);
}
return sum;
}
public static void main(String[] args) {
System.out.println(trap(new int[]{0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1})); // 6
System.out.println(trap(new int[]{4, 2, 0, 3, 2, 5})); // 9
}
//往单调栈里存入柱子数据的时候,不光是要柱子高度,还需要柱子的索引位置
//创建一个新的类型包含当前放入单调栈的柱子高度以及柱子索引位置
static class Data{
int height; //高度
int i; // 索引
public Data(int height, int i) {
this.height = height;
this.i = i;
}
@Override
public String toString() {
return String.valueOf(height);
}
}
}