1 单调栈特点
单调栈顾名思义,栈中的数据是递增或者是递减的,具体而言
- 单调递增栈:栈中数据出栈的序列为单调递增序列
- 单调递减栈:栈中数据出栈的序列为单调递减序列
2 单调栈的使用
【题目1】
给定一个不含有重复值的数组 arr,找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置。返回所有位置相应的信息。
【举例】 arr = {3,4,1,5,6,2,7} 返回如下二维数组作为结果:
【解题】
具体操作:
遍历数组元素,当遍历到数组的 arr[ i ] 元素时,
public static int[][] getLefAndRightSmall(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<Integer> stack = new Stack<>();// 单调减栈
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[i] < arr[stack.peek()]) {
int m = stack.pop();
res[m][1] = i;
res[m][0] = stack.isEmpty() ? -1 : stack.peek();
}
stack.push(i);// 栈为空或者 arr[i] 大于栈顶元素,则直接入栈
}
while(!stack.isEmpty()){
int m = stack.pop();
res[m][1] = -1;//单调减栈,所以右侧没有比栈顶元素小的数据
res[m][0] = stack.isEmpty() ? -1 : stack.peek();
}
return res;
}
【题目2】
题目1的升级版,数组中存在相同的元素。
【解题】
解法与题目1的解法相同,我们将相同的数据放入到栈的同一层中。例如数组{1,2,2,4},构建出来的栈结构如下
栈中数据 | 对应数组数据 |
3 | 4 |
1,2 | 2,2 |
0 | 1 |
public static int[][] getLefAndRightSmall2(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<LinkedList<Integer>> stack = new Stack<>();// 单调减栈 相同值放到栈的同一层 妙啊!!!
for (int i = 0; i < arr.length; i++) {
while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]) {
LinkedList<Integer> temp = stack.pop();// 使用LinkedList纯粹是为了方便取到最后一个数据,ArrayList居然没有直接获取最后的数据的Api
for (int j : temp) {
res[j][1] = i;
res[j][0] = stack.isEmpty() ? -1 : stack.peek().getLast();
}
}
// 将当前的i加入到栈中 需要考虑相等的情况
if(!stack.isEmpty() && arr[stack.peek().get(0)]== arr[i]){
stack.peek().add(i);
}else {
LinkedList<Integer> list = new LinkedList<>();
list.add(i);
stack.push(list);
}
}
while (!stack.isEmpty()) {
LinkedList<Integer> temp = stack.pop();
for (int j : temp) {
res[j][1] = -1;
res[j][0] = stack.isEmpty() ? -1 : stack.peek().get(0);
}
}
return res;
}
【题目3】
柱状图的最大矩形。力扣84题
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
【解题】暴力破解我们可以遍历每根柱子,以当前柱子 i 的高度作为矩形的高,那么矩形的宽度边界即为向左找到第一个高度小于当前柱体 i 的柱体,向右找到第一个高度小于当前柱体 i 的柱体。就是说 以当前柱子 i 的高度作为矩形的高 向左边和向右边 扩展开的最大宽度的边界 是 比当前柱体高度要小的主体,那么问题就简化成 【题目1】 即求解每个元素左边和右边最近的较小的值。
【代码如下】
/**
* 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。求在该柱状图中,能够勾勒出来的矩形的最大面积。
*/
public static int largestRectangleArea(int[] heights) {
int maxRectangleArea = 0;
int[][] nextLeftAndRight = new int[heights.length][2];
Stack<LinkedList<Integer>>stack = new Stack<>();
// 求每个矩形高相邻的最近的比它小的值
for(int i = 0; i < heights.length; i++){
while(!stack.isEmpty() && heights[i] < heights[stack.peek().getFirst()]){
// 0 1 2 3 4 5 6 7 8 9 10
// {3,4,1,1,5,5,6,2,7,7,7}
LinkedList<Integer> topList = stack.pop();
for(int j : topList){
nextLeftAndRight[j][1] = i;
nextLeftAndRight[j][0] = stack.isEmpty() ? -1 : stack.peek().getLast();
}
}
if(!stack.isEmpty() && heights[i] == heights[stack.peek().getFirst()]){
stack.peek().add(i);
}else {// 栈为空时 直接加入
LinkedList<Integer> list = new LinkedList<>();
list.add(i);
stack.push(list);
}
}
while(!stack.isEmpty()){
LinkedList<Integer> topList = stack.pop();
for(int j : topList){
nextLeftAndRight[j][1] = -1;// 设置
nextLeftAndRight[j][0] = stack.isEmpty() ? -1 : stack.peek().getLast();
}
}
for (int i = 0; i < heights.length; i++) {
int left = nextLeftAndRight[i][0];
int right = nextLeftAndRight[i][1] == -1 ? heights.length : nextLeftAndRight[i][1];
int newMax = heights[i] * (right - left -1);
maxRectangleArea = maxRectangleArea > newMax ? maxRectangleArea : newMax ;
}
return maxRectangleArea;
}
【优化1】
上面的代码冗余的处理在于 对于相同的数据 采用了【题目2】 相同的做法是将数据放到了栈的统一层,仔细思考我们发现相同高度的主体向左 和向右扩展开来的最大宽度是一样的,所以对于相同的元素我们可以直接入栈,直到遇到小于这些相同的元素的第一个数据时,在进行弹出操作,只需要计算相同元素的最后一个数据构成的最大矩形面积即可。
例如 数组 . [ 2, 1, 2, 2, 1 ],当我们遍历到最后一个元素1时,栈中存放的数组索引对应数据为 1 2 2 ,我们依次进行出栈直到找到比当前元素要小的数据 或者栈为空位置 这样 栈中的两个2 都会弹出 我们只需要计算第一个2 构成最大矩形即可。
【代码】
public int largestRectangleArea(int[] heights) {
int maxSize = 0;
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < heights.length; i++) {
while(!stack.isEmpty() && heights[i] < heights[stack.peek()]){
int top = stack.pop();
int lefIndex = stack.isEmpty() ? -1 : stack.peek();
maxSize = Math.max(maxSize, heights[top] * (i - lefIndex -1));// 注意这里面 每一个弹出的栈顶的 右边的元素较小的数据索引都是 i,不是 top - left
}
stack.push(i);
}
while(!stack.isEmpty()){
int top = stack.pop();
int lefIndex = stack.isEmpty() ? -1 : stack.peek();
maxSize = Math.max(maxSize, heights[top] * (heights.length - lefIndex -1));//这里也需要注意 是用length - leftIndex,不是length- top
// 例如 2 1 2 计算1 的左索引时 不能已1所在索引为边界,实际情况是 1左侧没有比它还小的数据,所以leftIndex不是 stack.pop的元素(当前正在计算的高的索引)
}
return maxSize;
}
【优化2】
为了简化代码,在2的版本上继续简化代码,
* 设置数组元素最左边值为0,可以不用判断栈中数据是否为空时计算得到索引为-1
* 设置数组元素最右边值为0,最后的数据0入栈时可以将栈中数据全部弹出,不用再进行单独的弹出栈进行操作
public static int largestRectangleArea4(int[] heights) {
// 这里为了代码简便,在柱体数组的头和尾加了两个高度为 0 的柱体。
int[] tmp = new int[heights.length + 2];
System.arraycopy(heights, 0, tmp, 1, heights.length);
Deque<Integer> stack = new ArrayDeque<>();
int area = 0;
for (int i = 0; i < tmp.length; i++) {
// 对栈中柱体来说,栈中的下一个柱体就是其「左边第一个小于自身的柱体」;
// 若当前柱体 i 的高度小于栈顶柱体的高度,说明 i 是栈顶柱体的「右边第一个小于栈顶柱体的柱体」。
// 因此以栈顶柱体为高的矩形的左右宽度边界就确定了,可以计算面积🌶️ ~
while (!stack.isEmpty() && tmp[i] < tmp[stack.peek()]) {
int h = tmp[stack.pop()];
area = Math.max(area, (i - stack.peek() - 1) * h);
}
stack.push(i);
}
return area;
}
个人推荐使用优化版2。
【题目4】
求最大子矩阵的大小
解题思路: 这一题的算法本质上和【题目3】 Largest Rectangle in Histogram一样,对每一行都求出每个元素对应的高度,这个高度就是对应的连续1的长度,然后对每一行都更新一次最大矩形面积。那么这个问题就变成了【题目3】 的 Largest Rectangle in Histogram。本质上是对矩阵中的每行,均依次执行84题算法。 如果矩阵的大小为 O(N×M),可以做到时间复杂度为 (N×M)。
注意到从第一行到第二行,height 数组的更新是十分方便的,即 height[j] = map[i][j]==0 ? 0 : height[j]+1。
3 根据得到的高度矩阵来求解最大的子矩阵,解法过程与【题目3】相同。
代码
/**
* 最大子矩阵面积
*/
public static int maxRecSize(int[][] map) {
if (map == null || map.length == 0 || map[0].length == 0) {
return 0;
}
int[] heights = new int[map[0].length];
int maxArea = 0;
for (int i = 0; i < map.length; i++) {// map.length 表示以为数组的个数,即列的个数
// 求出每一行的最大高度
for (int j = 0; j <map[0].length; j++) {
heights[j] = map[i][j] == 0 ? 0 : heights[j] + 1;// 这里新的一行在求解高度时 利用了前一行的高度值
}
maxArea = Math.max(maxArea, largestRectangleArea(heights));
}
return maxArea;
}
public static int largestRectangleArea(int[] heights) {
int maxSize = 0;
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < heights.length; i++) {
while(!stack.isEmpty() && heights[i] < heights[stack.peek()]){
int top = stack.pop();
int lefIndex = stack.isEmpty() ? -1 : stack.peek();
maxSize = Math.max(maxSize, heights[top] * (i - lefIndex -1));// 注意这里面 每一个弹出的栈顶的 右边的元素较小的数据索引都是 i,不是 top - left
}
stack.push(i);
}
while(!stack.isEmpty()){
int top = stack.pop();
int lefIndex = stack.isEmpty() ? -1 : stack.peek();
maxSize = Math.max(maxSize, heights[top] * (heights.length - lefIndex -1));//这里也需要注意 是用length - leftIndex,不是length- top
// 例如 2 1 2 计算1 的左索引时 不能已1所在索引为边界,实际情况是 1左侧没有比它还小的数据,所以leftIndex不是 stack.pop的元素(当前正在计算的高的索引)
}
return maxSize;
}