# 单调栈

# 面试场上超过窗口最大值概率5倍的结构.

# 单调栈是什么?

一种特别设计的栈结构,为了解决如下的问题:

给定一个可能含有重复值的数组arr,i位置的数一定存在如下两个信息

  1. arr[i]的左侧离i最近并且小于(或者大于)arr[i]的数在哪?

  2. arr[i]的右侧离i最近并且小于(或者大于)arr[i]的数在哪?

    如果想得到arr中所有位置的两个信息,怎么能让得到信息的过程尽量快。

如果是暴力方法,那得是O(N^2),但是单调栈可以做到O(N).

# 单调栈结构实现

假设无重复值

  1. 准备一个栈.要求,从下往上,是从小到大的,
  2. 来一个新的数,如果是空的,或者栈顶比他小,那么直接压栈.
  3. 如果新来的数比栈顶比他大,那么弹出栈顶,此时收集弹出数的答案,让他弹出的,就是他右侧比他小的,他压着的,就是他左边比他小的.
  4. 遍历完了,如果栈中还有数据,依次弹出,右侧没有被迫让他弹出的,右侧是-1,他压着的就是他左侧离他最近的比他小的,如果没有,也是-1.
  5. image-20240613232534726
// 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();
        //我压着的就是左边最近的比我小的,没有了就是-1
         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;
}

假设有重复值

不直接放下标了,组成一个小链表放进去.

image-20240612225241317

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;
}

# 证明

假设无重复值

  1. 假如栈里,b压着a,此时来了个c,且c<b,根据规则,他会让b弹出.

|b|

|a|

  1. 右边的证明:c是我遍历到的数,而b,已经在栈里了,所以,b一定在c的左边,那么b~c中间的数,不可能小于b,否则b就不会被c释放掉,而是被中间某个数释放.
  2. 左边的证明:a,b都在栈里,证明a,在b的左边,那么a~b之间的数,有没有可能<a,不可能,否则,a会被这个数释放掉,有可能a<?<b吗?不可能,因为如果是这样,那么a,b中间一定隔着某个数.因为释放不了.所以,中间的数,一定大于b,这些数都被b释放了.

# 正数子数组累加和 × 这个子数组中最小值 之后的最大值问题

给定一个只包含正数的数组arr,arr中任何一个子数组sub, 一定都可以算出(sub累加和 )* (sub中的最小值)是什么, 那么所有子数组中,这个值最大是多少?

  1. 核心思路,就是如果最小值固定了,那让累加和尽量的大,就可以得到最大值.

我让每个数都分别当最小值,然后,找到以这个数能扩到的最大的数组范围,算一个乘法值,然后最后比比哪个值更大.返回即可.

这就是一个单调栈,获取一个数,左右两侧比我小的位置以内的最大范围,累加和,✖️自己.

  1. 做一个前缀和数组,因为我要快速的知道我当前的范围内的累加和是多少.

# 时间复杂度

  1. 计算前缀和数组.O(N)
  2. 单调栈,生成每个位置左右位置O(N)
  3. 针对每个位置,查出累加和O(1),*最小值O(1),算N次,所以O(N).

# 单调栈算法

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();
         //i位置数使栈中元素弹出时,i前一个位置肯定就是j位置作为最小值的右侧最大边界了,所以sums[i-1]就是到右侧极限最大范围子数组的累加和,如果栈中有数据,说明从他的左侧往右才是以他为最小值的范围,所以累加和减去栈顶位置的下标的前缀和值.
         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;
}

# 直方图最大面积问题

给定一个非负数组arr,代表直方图 返回直方图的最大长方形面积

image-20240613232552001这个题解题思路,就是每个格子的高度做高,左右能扩到最大的宽度*自己的高度,跟上面那个题没什么区别.

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;
}

# 全部由1组成的最大子矩阵问题

这是个力扣原题 https://leetcode.com/problems/maximal-rectangle/ 难倒众多学员的一个德高望众的题.

给定一个二维数组matrix,其中的值不是0就是1, 返回全部由1组成的最大子矩形,内部有多少个1

image-20240613232802735

# N*N的大矩阵,有多少子矩阵(复杂度)?

N^4个.随便点一个点是N^2,再随便点一个点,是N^2,所以他俩组成的矩阵,是N^4.

暴力解:我先列举出有多少个子矩阵,已经是N^4,再验证这个每个子矩阵是不是都1,再来N^2,综合.O(N^6)

# 优雅的解 压缩数组+ 单调栈

image-20240613234049123

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++) {
         //如果当前行的有0,那就重置,否则+1
         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;
}

# 返回全部由1组成的子矩形数量

给定一个二维数组matrix,其中的值不是0就是1,返回全部由1组成的子矩形数量

  1. 假设到总矩阵某一层,压缩数组是[5,1,4,2,2,1]时

    image-20240614233204402

  2. 0→5先压栈,

  3. 1→1来了,他迫使5出栈,因为让他出来的1不等于5,此时计算5~2层的,左右不可达范围内所有结果,此时就是ABCD区域.

  4. 1→1压栈,2→4来了,他比1大,所以直接进栈.

  5. 3→2来了,他迫使2→4出栈,因为让他出来的4不等于2,此时计算4~3层的,左右不可达范围内所有结果,此时就是GH区域.

  6. 4→2来了,他迫使3→2出栈,但是此时出来的2等于自己的2,此时不计算,等着后来这个2出来时候统一计算,

  7. 5→1来了,他迫使4→2出栈,此时2!=1,计算4→2的,2~2层,左右不可达位置所有结果,此时就是IKM区域.

  8. 后面没有了,栈中还有数,弹出,此时5→1出栈,左边不可达-1,右边也没有比我小的,计算1~1层,左右不可达位置所有结果,此时就是EFJLNX区域.

  9. 至此,所有格子都收集过了.'

  10. 特别声明格子的计算方法:以0列为例子,他只有1列.根据公式(X-max(Y,Z)) * ((L * (L+1))/2)

    1. 公式计算方式

      image-20240614233241910

    2. 当遍历到第一行的时候,收集的就是A (1-0) * 1个

    3. 当遍历到第二行的时候,收集的就是B,AB (2-0) * 1个

    4. 当遍历到第三行的时候,收集的就是C,BC,ABC (3-0) * 1个

    5. 当遍历到第四行的时候,收集的就是D,CD,BCD,ABCD (4-0)*1个

    6. 当遍历到第五行的时候,收集的就是DE,CDE,BCDE,ABCDE,(5-1)*1个

    7. 为什么第五行,要减一,因为最底下的格子等着X位置联通成整个区域的时候才计算呢.

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;

}

// 比如
//              1
//              1
//              1         1
//    1         1         1
//    1         1         1
//    1         1         1
//             
//    2  ....   6   ....  9
// 如上图,假设在6位置,1的高度为6
// 在6位置的左边,离6位置最近、且小于高度6的位置是2,2位置的高度是3
// 在6位置的右边,离6位置最近、且小于高度6的位置是9,9位置的高度是4
// 此时我们求什么?
// 1) 求在3~8范围上,必须以高度6作为高的矩形,有几个?
// 2) 求在3~8范围上,必须以高度5作为高的矩形,有几个?
// 也就是说,<=4的高度,一律不求
// 那么,1) 求必须以位置6的高度6作为高的矩形,有几个?
// 3..3  3..4  3..5  3..6  3..7  3..8
// 4..4  4..5  4..6  4..7  4..8
// 5..5  5..6  5..7  5..8
// 6..6  6..7  6..8
// 7..7  7..8
// 8..8
// 这么多!= 21 = (9 - 2 - 1) * (9 - 2) / 2
// 这就是任何一个数字从栈里弹出的时候,计算矩形数量的方式
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);
}