单调栈是什么及单调栈可以解决哪些问题?

346 阅读5分钟

1、单调栈的概念

  1. 对于一系列数,如果想得到每个数的左边离它最近的比它小的数和右边离它最近的比它小的数,或者想得到左边最近的比它大的数或者右边最近的比它大的数,则可以使用单调栈。
  2. 对于单调递增栈来说,由于栈中的数据是递增的,每当一个数来临时,如果栈顶的元素大于当前值,则当前栈顶元素需要出栈,再将当前元素入栈;否则直接将当前元素入栈。
  3. 对于递减栈来说,正好相反。
  4. 对于数组中的每个元素来说,都只入栈一次、出栈一次,因此总的时间复杂度是O(n)O(n)。如果不用单调栈,对于每个元素都采用中心扩展的方式,则总的时间复杂度是O(n2)O(n^2)

2、单调栈的一些题目

(1)单调栈的一般代码(不含有重复值的处理)

public static int[][] getNearLessNoRepeat1(int[] arr) {
    int[][] res = new int[arr.length][2];
    Deque<Integer> stack = new LinkedList<>();

    for (int i = 0; i < arr.length; i++) {
        while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) {
            int now = stack.pop();
            Integer left = stack.peek();
            int l = left == null ? -1 : left;
            res[now][1] = i;
            res[now][0] = l;
        }
        stack.push(i);
    }
    //上面是遍历过程
    //[0,1]
    //下面是如果遍历完成之后,栈中还有元素的情况
    while (!stack.isEmpty()) {
        Integer cur = stack.pop();
        res[cur][1] = -1;
        Integer left = stack.peek();
        int l = left == null ? -1 : left;
        res[cur][0] = l;

    }
    return res;
}

(2) 单调栈的一般代码(含有重复值的处理)

public static int[][] getNearLess1(int[] arr) {
    int[][] res = new int[arr.length][2];
    Deque<List<Integer>> stack = new LinkedList<>();
    for (int i = 0; i < arr.length; i++) {
        while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]) {
            List<Integer> now = stack.pop();
            int left = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
            for (Integer integer : now) {
                res[integer][1] = i;
                res[integer][0] = left;
            }
        }
        if (!stack.isEmpty() && arr[stack.peek().get(0)] == arr[i]) {
            stack.peek().add(i);
        } else {
            //小于
            List<Integer> list = new ArrayList<>();
            list.add(i);
            stack.push(list);
        }
    }

    while (!stack.isEmpty()) {
        List<Integer> cur = stack.pop();
        int left = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
        for (Integer i : cur) {
            res[i][1] = -1;
            res[i][0] = left;
        }
    }
    return res;
}

由于数组中含有重复值,则需要考虑当前栈顶的元素与当前元素相同时的情况。这种处理方法是在单调栈中不存储下标,转而存储一系列相等值的下标组成的链表

这种方法可以解决重复值的问题,但是浪费了大题的空间。

所以还有另一种不如此浪费空间的做法,就是使用两个栈,一个栈stack1是一个递增栈,并且里面可以存储重复值。另一个栈stack2是存储每一系列相同值的最后一个下标,用这种方式就可以区分左面小于当前值的下标是多少。

具体的代码可以见下面这个牛客网题目的解答代码。

(3)牛客网上的单调栈题目(含有重复值的处理)

题目链接为:单调栈结构(进阶)_牛客题霸_牛客网 (nowcoder.com)

解答代码:

import java.util.*;
import java.io.*;
public class Main {
    public static void main(String[] args) throws IOException{
        StreamTokenizer st = new StreamTokenizer(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        st.nextToken();
        int n = (int) st.nval;

        int[][] res = new int[n][2];
        int[] arr = new int[n];

        for (int i = 0; i < n; i++) {
            st.nextToken();
            arr[i] = (int) st.nval;
        }

        int[] stack1 = new int[n];
        int[] stack2 = new int[n];
        int stackSize1 = 0, stackSize2 = 0;

        for (int i = 0; i < n; i++) {
            while (stackSize1 != 0 && arr[stack1[stackSize1 - 1]] > arr[i]) {
                int cur = stack1[--stackSize1];
                res[cur][1] = i;
                //求左侧小于它的最近的下标的时候,用的是stack2中的值,stack1中的值因为含有重复的值,用不了
                int left = stackSize2 < 2 ? -1 : stack2[stackSize2 - 2];
                res[cur][0] = left;
                //得判断一下,弹出的这个栈顶和前一个元素是不是相同。 如果不同,stack2中的元素要减一个
                if (stackSize1 == 0 || arr[stack1[stackSize1 - 1]] != arr[cur]) {
                    stackSize2--;
                }
            }
            //至此,栈顶上比当前元素大的元素已经都弹出去了
            if (stackSize1 == 0 || arr[stack1[stackSize1 - 1]] != arr[i]) {
                //如果栈顶元素与当前的不同
                stack2[stackSize2++] = i;
                
            } else {
                //如果栈顶元素与当前的相同

                stack2[stackSize2 - 1] = i;
            }
            stack1[stackSize1++] = i;
        }
        while (stackSize1 != 0) {
            int cur = stack1[stackSize1 - 1];
            stackSize1--;
            res[cur][1] = -1;
            int left = stackSize2 < 2 ? -1 : stack2[stackSize2 - 2];
            res[cur][0] = left;
            if (stackSize1 == 0 || arr[stack1[stackSize1 - 1]] != arr[cur]) {
                //如果栈顶元素与当前的不同
                stackSize2--;
            }
        }

        for (int[] r : res) {
            sb.append(r[0]).append(' ').append(r[1]).append('\n');
        }
        System.out.print(sb.toString());
           
    }
}

LeetCode 1856. 子数组最小乘积的最大值

题目链接:1856. 子数组最小乘积的最大值 - 力扣(LeetCode)

题目

题目分析

题目求的是子数组的和 * 子数组的最小值 中的最大值。

  • 如果用暴力遍历来做,遍历子数组就需要O(n2)O(n^2)的时间复杂度,然后对每一个子数组,求数组的和与最小值。如果求最小值再遍历一次,则总的时间复杂度就达到了O(n3)O(n^3)
  • 如果用单调栈来做,对于数组中的每一个元素来说,用单调递增栈就可以求出:当该元素作为最小值时,其左右两边的子数组能扩展到的最远的位置。由于这个数组中的数全是正数,当该元素作为数组的最小值时,最小值已经确定了下来,以最小值作为中间的一个点,向左右两边扩展的越远,形成的这个子数组的数组和就越大,乘积也就越大。用这种方法需要对数组遍历一次,利用递增栈的特性求出每个元素左右两边最近的比它小的位置,这个过程时间复杂度是O(n)O(n)。求前缀数组的时间复杂度是O(n)O(n)。最终利用单调栈的结果和前缀数组求乘积的过程也是O(n)O(n)的时间复杂度。总体也是O(n)O(n)的。
  • 下面的代码时间复杂度也是O(n)O(n)的,和上面分析的思想一样,只不过做了一点常数优化处理。
代码
class Solution {
     public static  int maxSumMinProduct(int[] nums) {
        int len = nums.length;
        long[] sums = new long[len + 1];
        int[] stack = new int[len];
        int top = -1;
        long result = 0;
        for (int i = 0; i < len + 1; i++) {
            if (i < len)
                sums[i + 1] = sums[i] + nums[i];
            int temp = i == len ? 0 : nums[i];
            while (top != -1 && nums[stack[top]] >= temp) {
                int cur = stack[top--];
                int left = top == -1 ? -1 : stack[top];
                result = Math.max(result, nums[cur] * (sums[i] - sums[left + 1]));
            }
            stack[++top] = i;
        }
        return (int)(result % (1_000_000_007));

    }
}
运行结果

image.png

LeetCode84. 柱状图中最大的矩形

题目链接:84. 柱状图中最大的矩形 - 力扣(LeetCode)

image.png

image.png

分析

也是用单调栈的思路,从左向右遍历每一个高度,求以每一个高度作矩形的高时,能够围出的矩形的最大面积。

代码
class Solution {
    public int largestRectangleArea(int[] heights) {
        int len = heights.length;

        int[] stack = new int[len];
        int top = -1;
        
        int ans = 0;
        for (int i = 0; i < len; i++) {
            while (top != -1 && heights[stack[top]] >= heights[i]) {
                int cur = stack[top--];
                int right = i;
                int left = top == -1 ? -1 : stack[top];
                //[left + 1, right - 1]
                ans = Math.max(ans, heights[cur] * (right - 1 - left));
            }
            stack[++top] = i;
        }
        while (top != -1) {
            int cur = stack[top--];
            int left = top == -1 ? -1 : stack[top];
            int right = len;
            ans = Math.max(ans, heights[cur] * (right - 1 - left));
        }
        return ans;
    }
}
运行结果

image.png

LeetCode85. 最大矩形

85. 最大矩形 - 力扣(LeetCode) 和上题是同样的思路。

代码
import static java.lang.Math.max;
class Solution {
    public int maximalRectangle(char[][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;

        int[] arr = new int[col];
        int ans = 0;
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (matrix[i][j] == '0') {
                    arr[j] = 0;
                } else {
                    arr[j] += 1;
                }
            }
            ans = max(func(arr), ans);
        }
        return ans;
    }

    public int func(int[] heights) {
        int len = heights.length;

        int[] stack = new int[len];
        int top = -1;
        
        int ans = 0;
        for (int i = 0; i < len; i++) {
            while (top != -1 && heights[stack[top]] >= heights[i]) {
                int cur = stack[top--];
                int right = i;
                int left = top == -1 ? -1 : stack[top];
                //[left + 1, right - 1]
                ans = Math.max(ans, heights[cur] * (right - 1 - left));
            }
            stack[++top] = i;
        }
        while (top != -1) {
            int cur = stack[top--];
            int left = top == -1 ? -1 : stack[top];
            int right = len;
            ans = Math.max(ans, heights[cur] * (right - 1 - left));
        }
        return ans;
    }
}
运行结果

image.png

LeetCode1504. 统计全1子矩形

1504. 统计全 1 子矩形 - 力扣(LeetCode)

代码
class Solution {
    public int numSubmat(int[][] mat) {
        int row = mat.length;
        int col = mat[0].length;
        int ans = 0;
        int[] arr = new int[col];
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (mat[i][j] == 1) {
                    arr[j]++;
                } else arr[j] = 0;
            }
            ans += func(arr, col);

        }
        return ans;
    }

    int func(int[] arr, int len) {
        int[] stack = new int[len];
        int top = -1;
        int ans = 0;

        for (int i = 0; i < len; i++) {
            while (top != -1 && arr[stack[top]] >= arr[i]) {
                int cur = stack[top--];
                int right = i;
                int left = top == -1 ? -1 : stack[top];
                // [left + 1, right - 1]
                int l = right - 1 - left; //这是长度
                int h = Math.max(left == -1 ? 0 : arr[left], arr[right]);
                ans += ((l * (l + 1)) >> 1) * (arr[cur] - h);
            }
            stack[++top] = i;
        }

        while (top != -1) {
            int cur = stack[top--];
            int right = len;
            int left = top == -1 ? -1 : stack[top];
            int l = right - 1 - left; //这是长度
            int h = Math.max(left == -1 ? 0 : arr[left], 0);
            ans += ((l * (l + 1)) >> 1) * (arr[cur] - h);
        }
        return ans;
    }
}
运行结果

image.png

题解

自己写的题解

LeetCode907. 子数组的最小值之和

907. 子数组的最小值之和 - 力扣(LeetCode)

代码
class Solution {
    public int sumSubarrayMins(int[] arr) {
        int len = arr.length;
        int[] stack = new int[len];
        int top = -1;
        long ans = 0;
        for (int i = 0; i < len; i++) {
            while (top != -1 && arr[stack[top]] >= arr[i]) {
                int cur = stack[top--];
                int right = i;
                int left = top == -1 ? -1 : stack[top];
                // [left + 1, right - 1]:left + 1 , cur, left + 2,cur,  cur,cur
                ans += 1L * arr[cur] * (cur - left) * (right - cur) ;
                
            }
            stack[++top] = i;
        }

        while (top != -1) {
            int cur = stack[top--];
            int right = len;
            int left = top == -1 ? -1 : stack[top];
            ans += 1L * arr[cur] * (cur - left) * (right - cur);
            
        }

        return (int) (ans % 1_000_000_007);
    }
}
运行结果

image.png