单调栈数据结构

一、单调栈结构介绍

今天我们来分享一个非常有趣,很好玩的数据结构,叫做单调栈,先来分享一下单调栈是来解决什么问题的,他是解决这样问题的,比如说在一个数组中[3,4,2,6,1,7,0], 要生成什么样的信息呢,没一个位置,左边离他最近的,比他小的位置在哪,和每一个右边离他最近的,比它小的位置在哪。

//比如  0位置的 3 左边离他最近比他小的位置没有 -1  右边离他最近的比它小的 2 位置的2
//      1位置的 4 左边离他最近比他小的位置0位置的3  右边离他最近的比它小的 2 位置的2
//      2位置的 2 左边离他最近比他小的位置没有 -1  右边离他最近的比它小的 4 位置的1
//  等等

0->3     -1        2->2
1->4     0->3      2->2
2->2     -1        4->1

当然他也能够解决左右两边离你最近的比你大的,你搞定了怎么下,那相反就能搞定怎么比你大的。我们先准备一个栈,这个栈就是我们所谓的单调栈,我一定要干嘛,从栈底到栈顶,由小到大,谁由小到大,代表的数组由小到大。我们接着往下看,我们从左往右依次遍历,面对第一个位置,0位置的3,它能不能直接进入到单调栈里,可以,为啥,因为单调栈里没东西。接下来1位置的4,可不可以直接进栈里,是可以的,为啥,因为4进来他是落到3的上面的,他没有改变我这个单调栈由栈底到栈顶由小到大的规则,他就直接进,再来,下面有意思了,目前栈中情况如下,

            2->2
|        |
|        |                                         左          右
|        |                ----->       1->4       0->3        2->2     
| 1->4   |                             0->3        -1         2->2
| 0->3   |
|________|

这个2能不能落在4的上面,不能,因为2如果落在上面,就改变了这个单调栈由栈底到栈顶由小到大的事实了。好,现在就要有东西从栈里弹出了,在一个东西弹出的时候,它的信息就得到了,啥意思,现在1位置的4要弹出,生成 1位置 4的信息,怎么生成,它右边离他进的比它小的,就是当前让他出来的数,它左边离他最近比他小的,就是它底下压着的数。如上图右边,那么这个2能不能落在3上面呢,不能,因为落在上面还是改变事实了,所以3继续弹出,弹出时生成信息,右边离他最近最小的,是让他弹出的2位置的2,左边它压着的没数了,就是规定为 -1 。然后2位置的2一看,栈里没东西了,他就可以直接进了,然后下一个位置是3位置的6,那么它是比2大的,可以直接进入。接下来就按照上述步骤继续就行了,当数组遍历完之后,栈里如果还有东西,需要单独进行弹出,右边单独弹出而不是因为某个数字让他弹出的,所以右边位置没有,为 -1 ,左边就是他底下压着的数字。下面这是代码,咱先来一个简单一点的,无重复值版本的。

public static int[][] getNearLessNoRepeat(int[] arr){

        int[][] res = new int[arr.length][2];
        Stack<Integer> stack = new Stack<>();
        for (int i : arr) {
            while (!stack.isEmpty() && arr[stack.peek()] > arr[i]){
                Integer popIndex = stack.pop();
                res[popIndex][1]  = i;
                res[popIndex][0] = stack.isEmpty() ? -1 : stack.peek();
            }

            stack.push(i);
        }
        while (!stack.isEmpty()){
            Integer popIndex = stack.pop();
            res[popIndex][1] = -1;
            res[popIndex][0] = stack.isEmpty() ? -1 : stack.peek();

        }
        return res;
    }

看,是不是非常的简单,这是无重复值版本的,顾名思义,就是数组中没有重复值。那如果就是有重复值,我们该怎么办呢,也好办,在无重复值的栈里面,我们放的是下表索引,那在有重复值的Stack里面,我们放置一个位置的链表就可以了,如果遇到重复值,我们不弹出,将该值加在链表的最后边,结算上一个结果的时候也是,取下面链表的最后一个位置就行啦,代码如下。

这是可以有重复值的版本:

public static int[][] getNearLessRepeat(int[] arr){

        int[][] res = new int[arr.length][2];
        Stack<ArrayList<Integer>> stack = new Stack<>();
        for (int i : arr) {       // i -> arr[i]  进栈
            while (!stack.isEmpty() && arr[stack.peek().get(0)] > arr[i]){
                ArrayList<Integer> popIs = stack.pop();
                int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
                for (Integer popArr : popIs) {
                    res[popArr][1] = i;
                    res[popArr][0] = leftLessIndex;
                }
            }
            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()){
            ArrayList<Integer> popIs = stack.pop();
            int leftLessIndex = stack.isEmpty() ? -1 : stack.peek().get(stack.peek().size() - 1);
            for (Integer pop : popIs) {
                res[pop][1] = -1;
                res[pop][0] = leftLessIndex;
            }

        }
        return res;
    }

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

什么意思,假如说我随意给一个数组[    ......      ]  该数组里面有很多的子数组,这些提示一下,下标连续的叫子数组,这里也是下标连续的情况,  每个子数组都能求一个累加和,和有一个最小值,我们求累加和 *  最小值 最大的那个值是多少。举个栗子 arr [ 3, 4 , 1 ,2 ,5]  我们随便找一个子数组  假如说子数组  [ 4   1  2]  他们的累加和是 7 最小值 是 1,所以我们要的就是  7 * 1 = 7,而我们要的是这些子数组中最大的那个值是多少。那要怎么求呢, 我们可以这么想,  我们先拿 0 位置距离,我们把  0 位置的3 当最小值,我们把最小值是  0 位置 3 的子数组全找出来,两个位置  3 自己和 [3  4] 组成的数组[ 3 ] 自己呢,它的最小值 是 3,累加和 是 3  所以它的指标是 3* 3 = 9 ,而子数组[3 4] 它的累加和是 7  ,最小值 是 3  所以它的指标是 21  ,等我们依次遍历这个数组,分别求出 以每个位置为最小值的 子数组,然后求出他们的累加和,相乘,然后求最大的,答案不就出来了嘛,已知该数组都是正数,那么我们怎么求以该位置为最小值的子数组呢,我们刚刚不是学了单调栈嘛,我们找左边第一个比你小的,右边第一个你比小的,这不就是以你为最小值的最大的子数组嘛,因为都是正的,没有负数,所以,以该数组为最小值,越长的子数组,累加和越大。我们怎么快速的求出一个子数组的累加和,时间复杂度在 O(1),我们可以预先生成一个前缀和数组来实现。

public static int max(int[] arr){
        int size = arr.length;
        int[] sums = new int[size];
        sums[0] = arr[0];
        for (int i = 1; i < size; i++) {
            sums[i] = arr[i] + sums[i - 1];
        }
        int max = Integer.MIN_VALUE;
        Stack<Integer> stack = new Stack<>();
        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;
    }

有同学看完代码后就疑惑了,这写错了吧, 这道题是有重复值的呀,这里面的Stack为啥是按无重复值计算的呀,其实,这是一种改写,是这样的,[3,4,3,4,3,1]  ,其实,在这里面,2位置的3算出的答案其实是错误的,但是我们四位置的3出来的时候,是可以算对的,这里面 0位置的3,2位置的3和4位置的3其实是联通的,前面的算错了,无所谓,只要后面的能算对就行。这里我们对单调栈使用时错了错化处理。

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

什么意思,举个例子,假如说一个数组[3,2,4,2,5],如下图,0位置高度为3,1位置高度为2,2位置高度为4,3位置高度为2,4位置高度为5,求啥,我们要的是画圆圈的位置,面积为 10。        

那要怎么求呢,我们必须以0位置为高的长方形,左右位置能扩多远,左边为 -1 ,没发扩,右边是2,扎不进去,所以它的面积就是这三个格子,然后我们以1位置为高的长方型,我们往左右扩,看能扩多远,依次往下,就能找到最大面积了。一个很简单的单调栈问题,就直接上代码了

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

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

如下图,一个大矩形里,有很多小的由1组成的矩形,我们要求的,就是最多的由1组成的矩形的数量。

怎么解呢?我们先求子矩阵必须以第0行做地基的情况下,哪个子矩阵包含的1最多,你把这行拿出来,把它当成直方图,然后求出最多的矩形,然后把第一行拿出来,必须以第一行当地基,看成直方图,求面积最大的面积

我们分别以每一行做地基,每一次遇到0,就相当于地基断掉了,在下一行时重新算地基,我们把每一行当做一个直方图,去求最大面积。

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

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

这里,我们也用到了压缩矩阵的技巧,我们先分别算每一行的矩阵数量,然后把他们相加起来不就好了嘛,如下图

这样我们既不会多算,也不会少算,客观来看,最后加起来就是答案,那么每一行我们该怎么算啊,看下图,我们先看第一个位置其实就是求7的左边离他最近比他小的,7的右边离他最近比他小的。-1位置到不了,2位置到不了,因为6比它矮,所以,以他自己做高的内部全是1的矩形就是这7个格子组成的,接下来我们看以这6个格子做高的矩形,它到不了-1,他到不了3,所以它能够扩的距离,1位置和2位置是可达的。那么这个区域有多少个格子呢,有公式(L*(L+1))/2  这就是这个区域的格子数。

假设在4-12这个区域中高度是6,那么每一层的格子数都是L*(L+1)/2,高度为6的算,高度为5的算,高度为3的算, 高度为2的不算,那高度为2的啥时候算呢,等高度为2这些能联通的弹出的时候算。接下来我们做一个抽象化,假设a位置高度为x,左边离他最近的比它小的位置在b,高度为y,右边离他最近的比它小的位置在c高度为z, 那么x高度所有的格子为 (x-max(y,z))*(L * (L+1)/2)。

代码如下:

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值