单调栈的使用

1 单调栈特点

单调栈顾名思义,栈中的数据是递增或者是递减的,具体而言

  • 单调递增栈:栈中数据出栈的序列为单调递增序列
  • 单调递减栈:栈中数据出栈的序列为单调递减序列

2 单调栈的使用
【题目1】
  给定一个不含有重复值的数组 arr,找到每一个
i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置。返回所有位置相应的信息。

【举例】 arr = {3,4,1,5,6,2,7}  返回如下二维数组作为结果:

  {
     {-1, 2},
     { 0, 2},
     {-1,-1},
     { 2, 5},
     { 3, 5},
     { 2,-1},
     { 5,-1}
  }
【解题】
遍历数组并使用单调递减栈存储数据的索引,因为栈中的数据递减的(从栈顶到栈底), 所以对于栈中相邻的的两个元素来说,下面的数据 就是 上面的数据 在数组中左侧距离最近的较小的数据
具体操作:
遍历数组元素,当遍历到数组的 arr[ i ] 元素时,
1  如果栈为空或者 arr[ i ]  大于栈顶元素,将arr[ i ]  对应数组索引 i  加入栈中
2  如果arr[ i ]  小于栈顶元素(栈一定不为空),则栈顶元素右侧紧邻的较小数据就是arr[ i ]  ,栈顶下面的数据就是栈顶左侧紧邻的较小的数据,将栈顶数据依次弹出与 arr[ i ]比较,直到 栈为空  或者 栈顶元素小于arr[ i ]时,将 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},构建出来的栈结构如下

 

栈中数据对应数组数据
34
1,22,2
01
代码:

 

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】

求最大子矩阵的大小

给定一个整型矩阵 map ,其中的值只有 0 1 两种,求其中全是 1 的所有矩形区域中,最
大的矩形区域为 1 的数量。
例如:
1 1 1 0
其中,最大的矩形区域有 3 1 ,所以返回 3
再如:
1 0 1 1
1 1 1 1
1 1 1 0
其中,最大的矩形区域有 6 1 ,所以返回 6
【题解】

解题思路 这一题的算法本质上和【题目3】 Largest Rectangle in Histogram一样,对每一行都求出每个元素对应的高度,这个高度就是对应的连续1的长度,然后对每一行都更新一次最大矩形面积。那么这个问题就变成了【题目3】 的 Largest Rectangle in Histogram。本质上是对矩阵中的每行,均依次执行84题算法。 如果矩阵的大小为 O(N×M),可以做到时间复杂度为 (N×M)。
 
解法的具体过程如下。
1 .矩阵的行数为 N ,以每一行做切割,统计以当前行作为底的情况下,每个位置往上的 1 的数量。使用高度数组 height 来表示。
例如:
map = 1 0 1 1
           1 1 1 1
           1 1 1 0
以第 1 行做切割后, height={1,0,1,1} height[j] 表示在目前的底(第 1 行)的 j 位置往上(包 括 j 位置),有多少个连续的 1
以第 2 行做切割后, height={2,1,2,2} height[j] 表示在目前的底(第 2 行)的 j 位置往上(包 括 j 位置),有多少个连续的 1
注意到从第一行到第二行,height 数组的更新是十分方便的,即
height[j] = map[i][j]==0 ? 0 : height[j]+1
以第 3 行做切割后, height={3,2,3,0} height[j] 表示在目前的底(第 3 行)的 j 位置往上(包 括 j 位置),有多少个连续的 1
 
2 .对于每一次切割,都利用更新后的 height 数组来求出以当前行为底的情况下,最大的矩形是什么。那么这么多次切割中,最大的那个矩形就是我们要的答案。
整个过程就是如下代码中的 maxRecSize 方法。步骤 2 的实现是如下代码中的 maxRecFromBottom 方法。
下面重点介绍一下步骤 2 如何快速地实现,这也是这道题最重要的部分,如果 height 数组 的长度为 M ,那么求解步骤 2 的过程可以做到时间复杂度为 O ( M )
对于 height 数组,读者可以理解为一个直方图,比如 {3,2,3,0} ,其实就是如图 1-7 所示的直方图。

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

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值