单调栈是什么?
一种特别设计的栈结构,为了解决如下的问题:
给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息
1)arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?
2)arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪?
如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快。
那么到底怎么设计呢?
题目一
单调栈实现
没重复值的单调栈实现:
1)栈中元素存数组下标
2)栈按元素值从小到大排序
3)当新来的一个数A,比栈顶值更小时,栈顶元素B的右边最近且小的值为A,左边最近且最小的是栈中B压着的下一个数。
4)然后弹出栈顶元素B,元素A压栈,周而复始
// arr = [ 3, 1, 2, 3]
// 0 1 2 3
// [
// 0 : [-1, 1]
// 1 : [-1, -1]
// 2 : [ 1, -1]
// 3 : [ 2, -1]
// ]
public static int[][] getNearLessNoRepeat(int[] arr) {
int[][] res = new int[arr.length][2];
// 只存位置!
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) { // 当遍历到i位置的数,arr[i]
while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
int j = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
res[j][0] = leftLessIndex;
res[j][1] = i;
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek();
res[j][0] = leftLessIndex;
res[j][1] = -1;
}
return res;
}
有重复值的单调栈实现
与没重复值实现不同的是
1)栈中存的是一个链表,将相同值放入同一个链表中
2)如果新来的元素A比栈顶元素小,使得栈顶元素B弹出,则B的右边最近较小值就是A,左边最近较小值,为其栈中压在下面的链表最后一个值
public static int[][] getNearLess(int[] arr) {
int[][] res = new int[arr.length][2];
Stack<List<Integer>> stack = new Stack<>();
for (int i = 0; i < arr.length; i++) { // i -> arr[i] 进栈
while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
List<Integer> popIs = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = i;
}
}
if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
stack.peek().add(Integer.valueOf(i));
} else {
ArrayList<Integer> list = new ArrayList<>();
list.add(i);
stack.push(list);
}
}
while (!stack.isEmpty()) {
List<Integer> popIs = stack.pop();
int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
for (Integer popi : popIs) {
res[popi][0] = leftLessIndex;
res[popi][1] = -1;
}
}
return res;
}
题目二
给定一个只包含正数的数组arr,arr中任何一个子数组sub,
一定都可以算出(sub累加和 )* (sub中的最小值)是什么,
那么所有子数组中,这个值最大是多少?
//暴力求解
public static int max1(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
int minNum = Integer.MAX_VALUE;
int sum = 0;
for (int k = i; k <= j; k++) {
sum += arr[k];
minNum = Math.min(minNum, arr[k]);
}
max = Math.max(max, minNum * sum);
}
}
return max;
}
单调栈求解:
1)找出以i位置为最小值的最大子数组
2)然后算出以i位置为最小值这个最大子数组的累加和sum*arr[i]
3)求所有位置为最小值累加和sum*arr[i]中最大的那个
public static int max2(int[] arr) {
int size = arr.length;
int[] sums = new int[size];
sums[0] = arr[0];
for (int i = 1; i < size; i++) {
sums[i] = sums[i - 1] + arr[i];
}
int max = Integer.MIN_VALUE;
Stack<Integer> stack = new Stack<Integer>();
for (int i = 0; i < size; i++) {
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[i - 1] : (sums[i - 1] - sums[stack.peek()])) * arr[j]);
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
max = Math.max(max, (stack.isEmpty() ? sums[size - 1] : (sums[size - 1] - sums[stack.peek()])) * arr[j]);
}
return max;
}
题目三
https://leetcode.com/problems/largest-rectangle-in-histogram
给定一个非负数组arr,代表直方图
返回直方图的最大长方形面积
1)求以i位置为高的时候的最大长方形面积
2)利用单调栈很容易的得到其以i位置为高的最左边界和最右边界
3)求所有位置为高的长方形面积中的最大值
//利用系统提供的栈结构
public static int largestRectangleArea1(int[] height) {
if (height == null || height.length == 0) {
return 0;
}
int maxArea = 0;
Stack<Integer> stack = new Stack<Integer>();
for (int i = 0; i < height.length; i++) {
while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
int j = stack.pop();
int k = stack.isEmpty() ? -1 : stack.peek();
int curArea = (i - k - 1) * height[j];
maxArea = Math.max(maxArea, curArea);
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
int k = stack.isEmpty() ? -1 : stack.peek();
int curArea = (height.length - k - 1) * height[j];
maxArea = Math.max(maxArea, curArea);
}
return maxArea;
}
//使用数组来实现自己的栈结构
public static int largestRectangleArea2(int[] height) {
if (height == null || height.length == 0) {
return 0;
}
int N = height.length;
int[] stack = new int[N];
int si = -1;
int maxArea = 0;
for (int i = 0; i < height.length; i++) {
while (si != -1 && height[i] <= height[stack[si]]) {
int j = stack[si--];
int k = si == -1 ? -1 : stack[si];
int curArea = (i - k - 1) * height[j];
maxArea = Math.max(maxArea, curArea);
}
stack[++si] = i;
}
while (si != -1) {
int j = stack[si--];
int k = si == -1 ? -1 : stack[si];
int curArea = (height.length - k - 1) * height[j];
maxArea = Math.max(maxArea, curArea);
}
return maxArea;
}
题目四
https://leetcode.com/problems/maximal-rectangle/
给定一个二维数组matrix,其中的值不是0就是1,
返回全部由1组成的最大子矩形,内部有多少个1
1)求以第i层为地基时的直方图
2)遇到0时,该层该位置的直方图为0
3)最后就化解成求直方图的最大面积
public static int maximalRectangle(char[][] map) {
if (map == null || map.length == 0 || map[0].length == 0) {
return 0;
}
int maxArea = 0;
int[] height = new int[map[0].length];
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[0].length; j++) {
height[j] = map[i][j] == '0' ? 0 : height[j] + 1;
}
maxArea = Math.max(maxRecFromBottom(height), maxArea);
}
return maxArea;
}
// height是正方图数组
public static int maxRecFromBottom(int[] height) {
if (height == null || height.length == 0) {
return 0;
}
int maxArea = 0;
Stack<Integer> stack = new Stack<Integer>();
for (int i = 0; i < height.length; i++) {
while (!stack.isEmpty() && height[i] <= height[stack.peek()]) {
int j = stack.pop();
int k = stack.isEmpty() ? -1 : stack.peek();
int curArea = (i - k - 1) * height[j];
maxArea = Math.max(maxArea, curArea);
}
stack.push(i);
}
while (!stack.isEmpty()) {
int j = stack.pop();
int k = stack.isEmpty() ? -1 : stack.peek();
int curArea = (height.length - k - 1) * height[j];
maxArea = Math.max(maxArea, curArea);
}
return maxArea;
}
题目五
https://leetcode.com/problems/count-submatrices-with-all-ones
给定一个二维数组matrix,其中的值不是0就是1,
返回全部由1组成的子矩形数量
1) 将二维数组的每一层转化为直方图形式
2)直方图下,以i位置为高时的区域内的矩形个数为:arr[i]*max(arr[L],arr[R])*(长度*长度+1)/2
3)最后算出所有层中的直方图总的矩形个数
public static int numSubmat(int[][] mat) {
if (mat == null || mat.length == 0 || mat[0].length == 0) {
return 0;
}
int nums = 0;
int[] height = new int[mat[0].length];
for (int i = 0; i < mat.length; i++) {
for (int j = 0; j < mat[0].length; j++) {
height[j] = mat[i][j] == 0 ? 0 : height[j] + 1;
}
nums += countFromBottom(height);
}
return nums;
}
public static int countFromBottom(int[] height) {
if (height == null || height.length == 0) {
return 0;
}
int nums = 0;
int[] stack = new int[height.length];
int si = -1;
for (int i = 0; i < height.length; i++) {
while (si != -1 && height[stack[si]] >= height[i]) {
int cur = stack[si--];
if (height[cur] > height[i]) {
int left = si == -1 ? -1 : stack[si];
int n = i - left - 1;
int down = Math.max(left == -1 ? 0 : height[left], height[i]);
nums += (height[cur] - down) * num(n);
}
}
stack[++si] = i;
}
while (si != -1) {
int cur = stack[si--];
int left = si == -1 ? -1 : stack[si];
int n = height.length - left - 1;
int down = left == -1 ? 0 : height[left];
nums += (height[cur] - down) * num(n);
}
return nums;
}
public static int num(int n) {
return ((n * (1 + n)) >> 1);
}
题目六
https://leetcode.com/problems/sum-of-subarray-minimums/
给定一个数组arr,
返回所有子数组最小值的累加和
//暴力求解
public static int subArrayMinSum1(int[] arr) {
int ans = 0;
for (int i = 0; i < arr.length; i++) {
for (int j = i; j < arr.length; j++) {
int min = arr[i];
for (int k = i + 1; k <= j; k++) {
min = Math.min(min, arr[k]);
}
ans += min;
}
}
return ans;
}
将题意转化为求:先求每一个以i位置为最小值的子数组的个数num*arr[i](这样转换可以很好的使用单调栈来求解)
然后再讲每个位置的num*arr[i]相加
// 没有用单调栈
public static int subArrayMinSum2(int[] arr) {
// left[i] = x : arr[i]左边,离arr[i]最近,<=arr[i],位置在x
int[] left = leftNearLessEqual2(arr);
// right[i] = y : arr[i]右边,离arr[i]最近,< arr[i],的数,位置在y
int[] right = rightNearLess2(arr);
int ans = 0;
for (int i = 0; i < arr.length; i++) {
int start = i - left[i];
int end = right[i] - i;
ans += start * end * arr[i];
}
return ans;
}
public static int[] leftNearLessEqual2(int[] arr) {
int N = arr.length;
int[] left = new int[N];
for (int i = 0; i < N; i++) {
int ans = -1;
for (int j = i - 1; j >= 0; j--) {
if (arr[j] <= arr[i]) {
ans = j;
break;
}
}
left[i] = ans;
}
return left;
}
public static int[] rightNearLess2(int[] arr) {
int N = arr.length;
int[] right = new int[N];
for (int i = 0; i < N; i++) {
int ans = N;
for (int j = i + 1; j < N; j++) {
if (arr[i] > arr[j]) {
ans = j;
break;
}
}
right[i] = ans;
}
return right;
}
//使用单调栈
public static int sumSubarrayMins(int[] arr) {
int[] left = nearLessEqualLeft(arr);
int[] right = nearLessRight(arr);
long ans = 0;
for (int i = 0; i < arr.length; i++) {
long start = i - left[i];
long end = right[i] - i;
ans += start * end * (long) arr[i];
ans %= 1000000007;
}
return (int) ans;
}
public static int[] nearLessEqualLeft(int[] arr) {
int N = arr.length;
int[] left = new int[N];
Stack<Integer> stack = new Stack<>();
for (int i = N - 1; i >= 0; i--) {
while (!stack.isEmpty() && arr[i] <= arr[stack.peek()]) {
left[stack.pop()] = i;
}
stack.push(i);
}
while (!stack.isEmpty()) {
left[stack.pop()] = -1;
}
return left;
}
public static int[] nearLessRight(int[] arr) {
int N = arr.length;
int[] right = new int[N];
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < N; i++) {
while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
right[stack.pop()] = i;
}
stack.push(i);
}
while (!stack.isEmpty()) {
right[stack.pop()] = N;
}
return right;
}