单调栈不是一种数据结构,可以理解成一个算法。
单调栈的能力:
可以使用空间换时间的方法,在时间复杂度为n的情况下,找到一个数组中,每一个数组左侧(或右侧),比该值小(或者大)的最临近的值以及其下标。
比如:
找到 10,3,7,4,12 中每个元素左侧的比其更小值的下标,通常需要循环两次n^2,但现在使用单调栈可以n的时间内完成识别。识别的结果为:-1,-1,1,1,3 其中-1代表左侧没有比其更小的值,正数代表下标。
单调递增栈和单调递减栈:
单调递增和单调递减,看的是栈顶到栈底是递增还是递减的。
怎么确定该使用哪个单调栈?(结论)
求左侧的更小的值,单调递减栈,数组正向遍历
求左侧的更大的值,单调递增栈,数组正向遍历
求右侧的更小的值,单调递减栈,数组反向遍历
求右侧的更大的值,单调递增栈,数组反向遍历
需要注意的是,如果存在1,1,1这种连续一样的数字,需要单独在做处理。
单调栈的原理:
举例:找到10,3,7,4,12中每一个节点的值的左侧的最小的值的位置。
定义一个普通的栈,并将其做成单调减栈,定义另外一个min[]数组,从左向右遍历,如果栈是空的,就把这个值压进栈中,把min[]对应的位置写成-1. 如果栈中有值,且压入的值大于peek栈顶的值,可以构成单调递减栈,因此值的下标入栈,继续遍历。 如果栈中有值,且压入的值小于peek顶的值,peek出栈。 如果栈中有值,且值和栈顶一样,需要根据实际情况判断怎么处理。
实现过程可以参考:https://blog.csdn.net/Zolewit/article/details/88863970
对应习题:LeetCode 84 柱状图中最大的矩形
标准答案中用了单调栈,但并不通用,我写了一个通用的方式,虽然看起来大,但很容易理解
package leetCode;
import java.util.Stack;
public class LeetCode_84 {
public static void main(String[] args) {
LeetCode_84 leetCode = new LeetCode_84();
int[] highs = {0, 1, 0, 1};
int maxVolumn = leetCode.largestRectangleArea(highs);
System.out.println(maxVolumn);
}
/**
* 非常经典的题目
* 1.如何找到最大的面积?
* 从头到尾遍历,以每一个值的高度为该范围的最大高度,左右两侧找高度小于等于它的柱子,找到以它为最高的最大面积
* 2.找左右两侧最大范围,很容易时间复杂度变成n^2,所以需要用最短的时间找到
* 使用单调栈的方法,找到每一个节点,最左侧和左右侧的最小值,然后计算该节点的最大体积
*
* @param heights 柱状体的高度
* @return 最大的存水体积
*/
public int largestRectangleArea(int[] heights) {
int length = heights.length;
// 获得当前每个节点,其左侧的最近的比其小的位置
// 使用正向的 单调递增栈
int[] leftMin = getLeftMinLocartion(heights);
// 获得当前每个节点,其右侧的最近的比其小的位置
// 使用反向的 单调递减栈
int[] rightMin = getRightMinLocartion(heights);
// 计算体积的时候,要算,左侧延伸的长度 + 1(自身长度) + 右侧延伸的长度
// 如果是-1,则可以延伸到最左或者最右
// 如果不是-1,则只能延伸一部分
int maxVolumn = 0;
for (int i = 0; i < length; i++) {
int leftSize = 0;
int rightSize = 0;
if (leftMin[i] == -1) {
leftSize = i; // 其实是i-0的简化
} else {
leftSize = i - leftMin[i] - 1;
}
if (rightMin[i] == -1) {
rightSize = length - 1 - i;
} else {
rightSize = rightMin[i] - i - 1;
}
int size = leftSize + 1 + rightSize;
int volumn = heights[i] * size;
if (volumn > maxVolumn) {
maxVolumn = volumn;
}
}
return maxVolumn;
}
// 找到每一个节点右侧比其更小位置,反向,单调减栈
private int[] getRightMinLocartion(int[] heights) {
Stack<Integer> stack = new Stack<>();
int[] rightMin = new int[heights.length];
int location = heights.length - 1; //反向,从右往左走
while (location >= 0) {
if (stack.isEmpty()) {
stack.push(location);
rightMin[location] = -1;
location--;
continue;
}
if (heights[location] < heights[stack.peek()]) {
stack.pop();
} else if ((heights[location] == heights[stack.peek()])) {
rightMin[location] = rightMin[stack.peek()];
stack.push(location);
location--;
} else {
rightMin[location] = stack.peek();
stack.push(location);
location--;
}
}
return rightMin;
}
// 找到每个点左侧的比其更小的位置,正向,单调递减栈(栈顶到栈底递减,栈顶更大)
private int[] getLeftMinLocartion(int[] heights) {
Stack<Integer> stack = new Stack<>();
int[] leftMin = new int[heights.length];
int location = 0;
while (location < heights.length) {
if (stack.isEmpty()) {
// 如果栈是空的,说明,该节点左侧没有比其更小的节点,因此
leftMin[location] = -1;
stack.push(location);
location++;
continue;
}
if (heights[stack.peek()] > heights[location]) {
stack.pop();
} else if (heights[stack.peek()] == heights[location]) {
leftMin[location] = leftMin[stack.peek()];
stack.push(location);
location++;
} else {
leftMin[location] = stack.peek();
stack.push(location);
location++;
}
}
return leftMin;
}
}