栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R

单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R

提示:重要基础算法!!
提示:重要基础算法!!
提示:重要基础算法!!

之前咱们刚刚学过单调栈1:
(1)单调栈1:o(1)复杂度求数组arr在窗口w内的最大值或者最小值

之前这个是系统双向队列实现的双向栈,适用于寻找L–R窗口内的最大值,最小值!
今天咱们来说系统栈实现的单调栈,适用于求数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R!

关于单调栈的知识点,一定要学会,今后很大互联网大厂的题目会用到这个俩基础的算法!
关于单调栈的知识点,一定要学会,今后很大互联网大厂的题目会用到这个俩基础的算法!
关于单调栈的知识点,一定要学会,今后很大互联网大厂的题目会用到这个俩基础的算法!


文章目录
  • 单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R
    • @[TOC](文章目录)
  • 高频题引入什么是单调栈2?
  • 一、审题
  • 暴力解宏观调度,每一个i收集一个答案
    • 这就是咱们案例中的结果: ![在这里插入图片描述](https://img-blog.csdnimg.cn/2f26e26ab8fb44aa915ccf8b39a8e77e.png)
  • 单调栈将复杂度优化到o(n)
    • 不妨设arr中没有重复的元素
    • 如果arr中有重复的元素,栈里面放列表,存相同的元素的位置们
  • 总结
高频题引入什么是单调栈2?

寻找数组arr中,每一个i=0–N-1位置,左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R
如果左边没有比i小的,那就-1,右边没有比i小的,那就-1


一、审题

示例:
arr=3 2 1 7 0 4 5 6
i = 0 1 2 3 4 5 6 7

每一个位置i,左边最近的比[i]小的位置L,右边最近的比[i]小的位置R
如下图所示


暴力解宏观调度,每一个i收集一个答案

来到i,咱们让L从i-1开始找,—找到i=0,如果都没有找到比[i]小的,那就是L=-1,否则就是L
与此同时,咱们让R从i+1开始找,—找到i=N-1,如果都没有找到比[i]小的,那就是L=-1,否则就是R

这样的话,每一个i都需要o(n)的时间复杂度寻找L,R
外围i从0–N-1遍历一遍需要o(n0
所以整体时间复杂度o(n^2),可不

代码可以写,用来验证优化算法是否正确,但是复杂度高,做一个了解:

//暴力解
    // 参数:输入:int[] arr,返回:所以位置的俩数int[]{a,b}左边,右边
    public static int[][] getLeftAndRightMinIndex(int[] arr) {
        int[][] res = new int[arr.length][2];
        if (arr == null || arr.length == 0) return res;//全0

        HashMap lMap = new HashMap<>();
        HashMap rMap = new HashMap<>();

        for (int i = 0; i < arr.length; i++) {
            //找左边的
            for (int j = i - 1; j >= 0; j--) {
                if (arr[j] < arr[i]) {
                    lMap.put(i, j);
                    break;
                }
                lMap.put(i, -1);//没有就填-1
            }
            //找右边的
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[i]) {
                    rMap.put(i, j);
                    break;
                }
                rMap.put(i, -1);//没有就填-1
            }
            //这里特殊值得考虑到位
            if (i == 0) lMap.put(i, -1);
            if (i == arr.length - 1) rMap.put(i, -1);
        }

        for (int k = 0; k < arr.length; k++) {
                res[k][0] = lMap.get(k);
                res[k][1] = rMap.get(k);
        }
        return res;
    }

    public static void test(){
        int[] arr = {3,2,1,7,0,4,5,6};
        int[][] res = getLeftAndRightMinIndex(arr);
        for (int k = 0; k < arr.length; k++) {
            for (int i = 0; i < 2; i++) {
                System.out.print(res[k][i]+" ");
            }
            System.out.println();
        }
    }

果不出所料,问题不大:

-1 1 
-1 2 
-1 4 
2 4 
-1 -1 
4 -1 
5 -1 
6 -1 
这就是咱们案例中的结果:
单调栈将复杂度优化到o(n)

上面说了,暴力解,来到i处,还需要咱们遍历寻找左右比[i]小的L,R
能否省掉这个过程?

当然可以!

利用系统栈的单调性解决,咱们设计一个从上到下是降序的单调栈stack!

每次进来的数必须保证是上大下小,如果来了个比栈顶还小的数x,自然把栈顶peek逼走弹出!
直到x比栈顶还大才能进栈。

注意,x进栈之前,先把刚刚弹出的栈顶结算了!
注意,x进栈之前,先把刚刚弹出的栈顶结算了!
注意,x进栈之前,先把刚刚弹出的栈顶结算了!

为啥要结算呢?
因为原本栈顶peek下面【peek下面压的自然就是L】,也就是数组中peek的左边最近还比自己小的位置L,就在peek这压着呢,
现在x来了,比栈顶peek小(也就是数组中peek的右边最近的比peek还小的x来了,【x自然就是R】)

巧不巧?
咱们要求的就是这俩啊:L,R,所以需要结算弹出的栈顶!!!
秒不妙???

这个结算栈顶的方式,每次只看peek下面压着谁?L, 新来的比我小的是谁?R,就o(1)复杂度搞定的事情。
——这就是单调栈牛逼的地方,把原来向左,向右搜索的过程,利用栈的单调性,优化并避免了搜索,直接拿下面,上面,结算了。

结果ans就是N*2维的数组,0列表示L,1列表示R,这easy,看案例就知道!

不妨设arr中没有重复的元素

好,上面的单调栈的思想,咱们理解了,
现在,咱们用例子来过一下,单调栈是如何优化到用o(n)复杂度搞定的
仍然是案例那个例子

arr=3 2 1 7 0 4 5 6
i = 0 1 2 3 4 5 6 7

看下图:单调栈,存i,上大下小
(1)i=0,进栈
(2)i=1,因为peek=0,i=1对应的2 然后i=1进栈
(3)i=2,因为peek=1,i=2对应的1 然后i=2进栈
(4)i=3,因为peek=2,i=3对应的7>peek=0对应的1,没有破坏了单调性,直接进栈;

(5)i=4,因为peek=3,i=4对应的0 然后i=4进栈
(6)i=5,因为peek=4,i=5对应的4>peek=4对应的0,没有破坏了单调性,i=5直接进栈;
(7)i=6,因为peek=5,i=6对应的5>peek=5对应的4,没有破坏了单调性,i=6直接进栈;
(8)i=7,因为peek=6,i=7对应的6>peek=6对应的5,没有破坏了单调性,i=7直接进栈;

arr遍历结束,单独处理栈
每一个栈顶上面都没有元素,所以大家的R都是=-1
每次结算栈顶peek时,它压着谁,谁就是左边L;
(9)结算peek=7的结果(peek=7压着6的),L=6,R-1;
(10)结算peek=6的结果(peek=6压着5的),L=5,R-1;
(11)结算peek=5的结果(peek=5压着4的),L=4,R-1;
(12)结算peek=4的结果(peek=4下面是空),L=-1,R-1;

整个过程走完,ans全部统计完成!——这个流程,希望你自己去画图,自己走一遍,否则你是无法理解的
咱们整个过程其实就在干一件事:
左边压着L,peek是要结算的栈顶,右边来了个R小于peek,就需要结算peek,
这样L就是左边最近小于peek的,R就是右边最近的小于peek的。


如果arr中有重复的元素,栈里面放列表,存相同的元素的位置们

好,理解了没有重复元素的那个流程

下面咱们来看真的任意数组arr,可能会有重复值
请问你咋办?

arr=3 2 1 7 7 7 0 4 5 6
i = 0 1 2 3 4 5 6 7 8 9

其实进栈结算的逻辑是跟上面一模一样的,只不过,栈里面存在不再是int类型的一个数,而是List列表!
列表放相同的arri的位置们(比如777的位置们3 4 5)
现在,来了i=6位置对应的0,显然破坏了栈的单调性,需要弹出栈顶的列表,
然后结算栈顶的list,结算时,所有的列表list的位置们(3 4 5),都需要结算哦!!!

比如:下图

弹出的list=3 4 5,他们全部的R=i=6,他们全部的左边是L=此时新栈顶那个list【2】的末端位置M-1那个,当然这里是0,因为M=1;
为啥是此时新栈顶那个list【2】的末端位置呢?
万一新栈顶pnew还有几个相同的1呢?比如是0 1 2 三个位置都是1,那3 4 5的左边就只能是L=2,因为2举例3 4 5 都是最近的小于3 4 5对应的7的。

OK捋清了吧!!

真正的单调栈,肯定要考虑数组arr是重复的情况,所以咱们来手撕代码实现上述流程吧!
(0)遍历每一个位置i,方便收集答案
(1)一上来,先看看需要弹出栈顶收集答案吗?需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
他们的R是此刻的i;
(2)(1)走完,咱们就要压栈i进去,注意,如果栈顶已经存在了arr[i],添加即可,否则要新建list
(3)如果i=N了那越界出来了,还要处理栈中剩下的元素们,结算他们,他们所有的R=-1;如果最新的栈顶pNew不空,则L就是pNew的末尾位置那个i,空的话L=-1;

整个过程就是保持栈顶peek左边小,右边小,切都是最近的位置i,结算peek也就很简单了!!!
手撕代码:

//复习:
    //真正的单调栈,肯定要考虑数组arr是重复的情况,所以咱们来手撕代码实现上述流程吧!
    public static int[][] getLeftRightNearestLessiPosition(int[] arr){
        if (arr == null || arr.length == 0) return null;

        int N = arr.length;
        int[][] ans = new int[N][2];//结果0L,1R
        //用栈搞定
        Stack> stack = new Stack<>();

        //(0)遍历每一个位置i,方便收集答案
        for (int i = 0; i < N; i++) {
            //(1)一上来,先看看需要弹出栈顶收集答案吗?因为每次我们面对的i都可能收集答案,除了第一次i=0
            while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]){
                // 需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
                //他们的R是此刻的i;
                List list = stack.pop();//先弹出来
                int R = i;
                int L = stack.isEmpty() ? - 1 : stack.peek().get(stack.size() - 1);//新栈顶的末尾位置
                for(Integer j:list){
                    ans[j][0] = L;
                    ans[j][1] = R;
                }
            }

            //(2)(1)走完,咱们就要压栈i进去
            //注意,如果栈顶已经存在了,添加即可,否则要新建list
            if (!stack.isEmpty() && arr[i] == stack.peek().get(0)){
                //有了
                stack.peek().add(i);
            }else {
                //没有的话
                List list = new ArrayList<>();
                list.add(i);
                stack.push(list);
            }
        }
        //(3)如果i=N了那越界出来了,还要处理栈中剩下的元素们,结算他们,
        // 他们所有的R=-1;如果最新的栈顶pNew不空,则L就是pNew的末尾位置那个i,空的话L=-1;
        while (!stack.isEmpty()){
            // 需要的话,看栈空吗?空:L=-1,否则L就是此时新栈顶pNew的末尾位置那个i
            //他们的R是此刻的i;
            List list = stack.pop();//先弹出来
            int R = -1;
            int L = stack.isEmpty() ? - 1 : stack.peek().get(stack.size() - 1);//新栈顶的末尾位置
            for(Integer j:list){
                ans[j][0] = L;
                ans[j][1] = R;
            }
        }

        return ans;
    }

逻辑清晰的话,写代码一点不是问题的!!!
测试一把:

public static void test2(){
        int[] arr = {3,2,1,7,0,4,5,6};
        int[] arr2 = {3,2,1,7,7,7,0,4,5,6};
        System.out.println("arr不重复的话:");

        int[][] res = getSubArrayNumWithSingleStack(arr);
        for (int k = 0; k < arr.length; k++) {
            for (int i = 0; i < 2; i++) {
                System.out.print(res[k][i]+" ");
            }
            System.out.println();
        }

        System.out.println();
        res = getLeftAndRightMinIndex(arr);
        for (int k = 0; k < arr.length; k++) {
            for (int i = 0; i < 2; i++) {
                System.out.print(res[k][i]+" ");
            }
            System.out.println();
        }

        System.out.println("arr重复的话:");

        res = getSubArrayNumWithSingleStack(arr2);
        for (int k = 0; k < arr.length; k++) {
            for (int i = 0; i < 2; i++) {
                System.out.print(res[k][i]+" ");
            }
            System.out.println();
        }

        System.out.println();
        res = getLeftAndRightMinIndex(arr2);
        for (int k = 0; k < arr.length; k++) {
            for (int i = 0; i < 2; i++) {
                System.out.print(res[k][i]+" ");
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
//        test();
        test2();
    }
arr不重复的话:
-1 1 
-1 2 
-1 4 
2 4 
-1 -1 
4 -1 
5 -1 
6 -1 

-1 1 
-1 2 
-1 4 
2 4 
-1 -1 
4 -1 
5 -1 
6 -1 
arr重复的话:
-1 1 
-1 2 
-1 6 
2 6 
2 6 
2 6 
-1 -1 
6 -1 

-1 1 
-1 2 
-1 6 
2 6 
2 6 
2 6 
-1 -1 
6 -1 

测试都没问题,所以不重复的arr那个流程理解了,理解重复的arr的代码就很简单了,最终咱们考虑arr是重复的。


总结

提示:重要经验:

1)单调栈的2种形式,一种是求窗口内的最大值最小值,一种是求i左边右边离i最近比i小的位置们
2)充分搞懂单调栈,解决大厂的题目,就游刃有余了!
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/884764.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号