单调栈结构以及算法中实际应用

一、什么是单调栈

单调栈就是一个栈结构,但是要求栈底到栈顶的元素必须是单调的(递增或者递减)。本身没啥屌的,但是用法非常屌。

二、看一个小题

有这样一个数组[3,5,2,4,6,0,1,5],现在要求,打印出每一个元素距离它两边的最近的比它大的值。看图片:

没有边界值的为null。暴力破解很容易,遍历每一个数组,然后两边查找比它大的值。有没有可能时间复杂度O(n)解法呢?

单调栈上场:

访问第一个元素,直接压栈

访问第二个元素的时候发现5比栈中3大,由于单调性的问题,所以弹出3,此时可以确定,元素3左边为null,右边就是5,因为只有遇到比栈顶元素大的元素,才会触发弹栈,这个时候就满足了条件!打印输出:元素3 : null,5

访问第三个元素的时候,符合条件直接压栈,访问第四个元素的时候,又比栈顶元素大,所以弹出2,此时打印输出:元素2:5,4。因为下面的就是当前元素最左边比它大的数。

同理,访问第五个元素的时候,现在栈中有4,5。所以全部弹栈,打印输出:元素4:5,6。打印输出:元素5:null,6。

当访问元素1的时候,0弹出。打印输出:元素0:6,1。

最后5入栈,然后数组遍历结束,但是栈还有元素,所以都需要弹出栈。5弹出栈,因为上面没有其他元素了,所以打印输出:元素5:6,null。最后是6:null,null。

这样时间复杂度就是O(n)。

    public static void print(int[] arr){
        if(arr == null || arr.length == 0){
            return ;
        }
        Stack<Integer> stack = new Stack<>();
        for(int i = 0;i < arr.length;i++){
            //弹栈
            while(!stack.isEmpty() && arr[stack.peek()] < arr[i]){
                //目标值
                int target = stack.pop();
                System.out.print("元素"+arr[target] + ",");
                if(stack.isEmpty()){
                    System.out.print("左:null" );
                }else{
                    System.out.print("左:"+arr[stack.peek()]);
                }
                System.out.println(",右:"+arr[i]);
            }
            stack.push(i);
        }
        while(!stack.isEmpty()){
            System.out.print("元素"+arr[stack.pop()]  + ",");
            if(stack.isEmpty()){
                System.out.print("左:null" );
            }else{
                System.out.print("左:"+arr[stack.peek()]);
            }
            System.out.println(",右:null");
        }
    }

这样大家应该了解了单调栈的作用。它总是能够找到距离它最近的并大于它的值。

三、再看一个小题

给定一个整型矩阵map,其中的值只有0和1两种,求其中全是1的所有矩阵区域中,最大的矩阵区域为1的数量。输入数组arr[n][m],要求时间复杂度O(n * m)。

例如:

1       1        1        0

返回3。

再如:

1       0        1        1

1       1        1        1

1       1        1        0

返回6。

是不是一脸懵逼?用单调栈就很好搞。我们先看另一个柱状图的问题。

求这个图形,最大的矩形格数,很明显是10格对吧。我们先用单调栈解决这个问题之后,就解决了上面的问题。

可以当成一个数组{4,3,2,5,6},此时单调栈单调性发生变化

指向4的时候,直接入栈。

指向三的时候,发现3小于栈顶元素4,所以弹出4,这个时候就可以得出4的左右边界分别是null和3的位置,下标分别为j = -1(代表null)和i =1,这个时候可以计算4* (i - j - 1)等于4。

然后接着遇到2。同理弹出3计算3的数值,2入栈,3的位置,左右边界分别为null和2的位置,下标分别为j = -1(代表null)和i =2,这个时候可以计算3 * (i - j - 1)等于6。

然后入栈5和6都符合条件

这个时候又是直接弹栈了:

弹出6,左右边界分别是5和null的位置,下标分别为j = 3(代表null)和i =5(代表右边null),这个时候可以计算6 * (i - j - 1)等于6。

弹出5,左右边界分别是2和null的位置,下标分别为j = 2(代表null)和i =5(代表右边null),这个时候可以计算5 * (i - j - 1)等于10。

弹出2,左右边界分别是null和null的位置,下标分别为j = -1(代表null)和i =5(代表右边null),这个时候可以计算6 * (i - j - 1)等于10。

这样就能够拿到最大值了,所以呢!对于原问题:我们从第一层数组遍历当成一个柱状图,然后再下一层加上上面层的值在做成柱状图解法:

第一层:数组{1,0,1,1}

第二层:数组{2,1,2,2}

第三层:数组{3,2,3,0}。每一层只要遇到0就直接记0。形成柱形结构,算出格数然后比较!!

看代码如下:

    public static int test(int[][] arr){
        if(arr == null || arr.length == 0 || arr[0].length == 0){
            return 0;
        }
        int max = 0;
        int[] temp = new int[arr[0].length];
        for(int i = 0;i < arr.length;i++){
            for(int j = 0;j < arr[0].length;j++){
                temp[j] = arr[i][j] == 0 ? 0 : temp[j] + 1;
            }
            max = Math.max(fuct(temp),max);
        }
        return max;
    }

    /**
     * 计算柱形图
     */
    public static int fuct(int[] arr){
        if(arr == null || arr.length == 0){
            return 0;
        }
        int max = 0;
        Stack<Integer> stack = new Stack<>();
        for(int i = 0;i < arr.length;i++){
            while(!stack.isEmpty() && arr[stack.peek()] > arr[i]){
                int now = stack.pop();
                int left = stack.isEmpty() ? -1 : stack.peek();
                int maxTemp = arr[now] * (i - left - 1);
                max = Math.max(max,maxTemp);
            }
            stack.push(i);
        }
        while(!stack.isEmpty()){
            int right = arr.length;
            int nowIndex = stack.pop();
            int left = stack.isEmpty() ? -1 : stack.peek();
            max = Math.max(max,arr[nowIndex] * (right - left - 1));
        }
        return max;
    }

四、大boos题

环形山问题

给定一个数组{1,2,4,5,3},可以重复。形成一个环。五座山,数值是山的高度,求有多少对山能相互看见?

相互看见:(1)相邻可以相互看见(2)两个节点间如果中间的值都小于等于两个节点中较小的一个说明能看见。

不能看见:两个节点中存在值大于两个节点中较小值。

上图符合条件的有:(1,2)(2,4)(4,5)(3,5)(1,3)(2,3)(3,4)。一共七对。

1、如果是数值无重复的情况

如果数组长度是1,那么返回0,如果是2,那么返回1。

可以做到时间复杂度O(1),直接有公式,2 * n -3,n是数组个数。

分析:

圆环上有无数个山峰,我们首先找到最高的和次高的,然后取到任意节点i,我们的思路是:有这个选定的小的节点去两边找离得最近的大的节点,i和左边最大的节点,i和右边最大的节点构成了两对符合要求的山峰。这样的i一共有多少个呢,一共有n-2个,去除了最高和次高。所以有 2 *(n - 2)对,然后算上最高和次高,所以呢最终有2 * (n - 2) + 1= 2n - 3。

2、如果是有重复情况

就不能上面那么做了,我们可以利用单调栈!

搞一个栈,这个栈里面不在记录数组的下标,而是数组的值和重复的次数就可以了。我们依然是从i找到相邻的两个大的数。只不过我们是从最大值作为起始值开始!!!假设山峰这样,我们从5开始

5入栈,次数是1

3入栈,次数是1,然后是一重复的4入栈,那么就是

这个时候遇到了5,发现不符合条件就要弹栈。因为4有重复所以重复,重复之间的山峰可以互相看到,3个4山峰就c(3,2),三个山峰中选出两个组合,然后在加上可以看到的两边的山峰,3个三峰都可以看到两边大的,所以就是3 * 2。最后结果就是result = 3 + 6 = 9

然后是3弹栈,同理 result += 0 + 2,最终等于11。然后5、4入栈:

数组遍历结束,然后开始清空栈,同时开始结算。

(1)倒数第三个元素及以上,依然是按照上面节算c(n,2) + 2 *n,因为可以看到最高和次高。

(2)倒数第二个元素的时候,实际上就是次高,次高需要判断最高是否有两个以上,如果有那么直接还是跟上面一样。如果只有一个,那么就是那么就是 c(n,2) + n。因为只有一个高峰,所有的次高只能看见一个。

(3)最高峰的时候,就只算自己了。

代码:

public class NumShan {

    public static class Pair{
        public int value;
        public int times;
        public Pair(int value ){
            this.times = 1;
            this.value = value;
        }
    }

    public static long communication(int[] arr){
        if(arr == null || arr.length < 2){
            return 0;
        }
        int size = arr.length;
        int maxIndex = 0;
        for(int i = 0; i < size;i++){
            maxIndex = maxIndex > arr[i] ? maxIndex : i;
        }
        int value = arr[maxIndex];
        int index = nextIndex(size,maxIndex);
        long res = 0L;
        Stack<Pair>  stack = new Stack<Pair>();
        stack.push(new Pair(value));
        //遍历过程中结算
        while(index != maxIndex){
            value = arr[index];
            while(!stack.isEmpty() && stack.peek().value < value){
                int times = stack.pop().times;
                // C(2,times) + 2 * times
                res += getInternalSum(times) + 2 * times;
            }
            if(!stack.isEmpty() && stack.peek().value == value){
                //如果重复
                stack.peek().times++;
            }else{
                //新节点
                stack.push(new Pair(value));
            }
            //下一个位置
            index = nextIndex(size,index);
        }
        //最后结算
        while(!stack.isEmpty()){
            int times = stack.pop().times;
            res += getInternalSum(times);
            //如果不是最高
            if(!stack.isEmpty()){
                //不是最高,最起码能看到最高
                res += times;
                //如果是倒数第三个元素及以上
                if(stack.size() > 1){
                    res += times;
                }else{
                    //如果是次高,需要判断最高的情况。
                    res += stack.peek().times > 1 ? times : 0;
                }
            }
        }
        return res;

    }

    /**
     * 排列组合
     */
    public static long getInternalSum(int n){
        return n == 1L ? 0L : (long) n * (long)(n - 1) / 2L;
    }
    public static int nextIndex(int size,int i){
        return i < (size - 1) ? (i + 1) : 0;
    }


}

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值