LeetCode高频题84. 柱状图中最大的矩形,单调栈求位置i左边右边距离i最近且比i位置小的位置,然后结算面积

发布于:2023-01-17 ⋅ 阅读:(439) ⋅ 点赞:(0)

LeetCode高频题84. 柱状图中最大的矩形,单调栈求位置i左边右边距离i最近且比i位置小的位置,然后结算面积

提示:本题是系列LeetCode的150道高频题,你未来遇到的互联网大厂的笔试和面试考题,基本都是从这上面改编而来的题目
互联网大厂们在公司养了一大批ACM竞赛的大佬们,吃完饭就是设计考题,然后去考应聘人员,你要做的就是学基础树结构与算法,然后打通任督二脉,以应对波云诡谲的大厂笔试面试题!
你要是不扎实学习数据结构与算法,好好动手手撕代码,锻炼解题能力,你可能会在笔试面试过程中,连题目都看不懂!比如华为,字节啥的,足够让你读不懂题
在这里插入图片描述

基础知识:
【1】单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R


题目

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积


一、审题

示例:

在这里插入图片描述
输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

在这里插入图片描述
输入: heights = [2,4]
输出: 4

提示:

1 <= heights.length <=105
0 <= heights[i] <= 104


二、解题

本质上就是求,以每个位置i的柱子为高度h时,咱们能往左右扩出多长的长度来(设为a长度)

面积s=a*h

这就是位置i做高时求得的面积

那你遍历一遍每个位置(N个),每个位置求一个S,将其更新给max
可不就是咱们要的最大的矩形面积吗?

这就是解题思路

单调栈确定每个位置i左边距离自己最近,且比自己小的那个位置L,右边距离自己最近且比自己小的位置R

a=R-L-1
比如下图
i=0时,咱们以3做高度h,能扩出最大的矩形是?
左边距离0最近的比3小的位置L=-1,
右边距离0最近的比3小的位置R=1
故矩形底边a=R-L+1=1-(-1)-1=2-1=1

所以最大矩形面积是S=a*h=1×3=3

这是以0位置作为高度的情况下,能扩出来的最大矩形
在这里插入图片描述

来到每一个位置i,单调栈确定每个位置i左边距离自己最近,且比自己小的那个位置L,右边距离自己最近且比自己小的位置R
这件事情的复杂度,需要o(1)

这样才能保证整体复杂度o(n)

这就是本题考你的地方
我之前花了很大心思讲过这个事情
【1】单调栈2:寻找数组arr中i位置左边距离i最近的比i小的位置L,右边距离i最近的比i小的位置R

那么就要求咱们提前准备空间,将我们要的LR提前保存好——当然了,如果你代码上要优化的话,另说

coding很巧妙

单调栈:保存下标,规则arri大数压小数,即栈底到栈顶是递增的

arr = 3 2
最开始把0位置(3)压栈

自然栈底L=-1
来了一个新的数1位置(2),要满足单调栈的话(我们规定栈底要小,栈顶要要大,懂?),就要把0位置(3)挤出去

一旦弹出!结算弹出点的面积
一旦弹出!结算弹出点的面积
一旦弹出!结算弹出点的面积

cur=0位置(3),显然此刻,0位置压着的L=-1就是左边距离自己最近,且比3小的位置
显然,是谁让cur弹出的?1位置(2)呗,R=1,它就是距离3最近,且比3小的位置

这时候,按照我们上面说的a=R-L-1=1-(-1)-1=1

结算面积S=a×h=1×3=3
更新给max=3,这是我们最好要比N次得到的最大面积
在这里插入图片描述

现在你明白了吧,单调栈弹出时立马结算,咱们接着看

多压点数咋搞?

arr = 3 4 5 6 2
最开始把0位置(3)压栈
然后,把1位置(4)压栈
然后,把2位置(5)压栈
然后,把3位置(6)压栈
为啥可以一直压,因为我们规定了单调栈的规则,就是下小,上大

在这里插入图片描述
此时,竟然来了4位置(2)
它比栈顶小,显然逼着栈顶们弹出了

一旦弹出,就要结算弹出节点cur的面积,更新给max
左边L是谁?栈不空自然就是cur压着那个点,比如cur=3位置(6)
cur压着谁呢?2位置(5),L=2
R是谁?自然就是逼迫cur弹出那个位置,可不就是现在的4位置(2),所以R=4
因此a=R-L-1=4-2-1=1
因此,cur=3位置为高,扩出来最大的矩形面积就是s=a*h=1×6=6
max=6

再看4位置(2)能进栈吗?不能
需要弹出栈顶cur=2位置(5)
在这里插入图片描述
一旦弹出,就要结算弹出节点cur的面积,更新给max
左边L是谁?栈不空自然就是cur=2压着那个点,
cur压着谁呢?1位置(4),L=1
R是谁?自然就是逼迫cur弹出那个位置,可不就是现在的4位置(2),所以R=4
因此a=R-L-1=4-1-1=2
因此,cur=2位置为高,扩出来最大的矩形面积就是s=a*h=2×5=10
max=10(比之前的6大)

再看4位置(2)能进栈吗?不能
需要弹出栈顶cur=1位置(4)
在这里插入图片描述

一旦弹出,就要结算弹出节点cur的面积,更新给max
左边L是谁?栈不空自然就是cur=1压着那个点,
cur压着谁呢?0位置(3),L=0
R是谁?自然就是逼迫cur弹出那个位置,可不就是现在的4位置(2),所以R=4
因此a=R-L-1=4-0-1=3
因此,cur=1位置为高,扩出来最大的矩形面积就是s=a*h=3×4=12
max=12(比之前的10大)

再看4位置(2)能进栈吗?不能
需要弹出栈顶cur=0位置(3)
在这里插入图片描述

一旦弹出,就要结算弹出节点cur的面积,更新给max
左边L是谁?栈不空自然就是cur=0压着那个点,
cur压着谁呢?栈为空,L=-1
R是谁?自然就是逼迫cur弹出那个位置,可不就是现在的4位置(2),所以R=4
因此a=R-L-1=4-(-1)-1=4
因此,cur=0位置为高,扩出来最大的矩形面积就是s=a*h=4×3=12
max=12(不比之前的12大)


如果arr有重复元素怎么办????没关系,依然按照上面的逻辑,不会错过最优解

arr = 3 4 3

0位置(3)进栈,
1位置(4)进栈,

来了2位置(3)
迫使1位置(4)进栈弹出cur=1,结算,s=1*4=4
max=4

这个按照上面玩就行
在这里插入图片描述
但是现在问题来了,2位置(3)进栈吗?因为不是严格递增的

不进栈!

让0位置(3)弹出,cur=0,结算
L=-1,R=2
a=2-(-1)-1=2
s=a×h=2×3=6
max=6

你这个s=6合理吗??????

其实你当前2位置的3是可以扩过来的啊
就是说,下面红色格子没问题,你咋就算了个6??
在这里插入图片描述
没关系,我们暂时来了一个次优解

这不是,你2位置的3立马要进栈吗???

在这里插入图片描述
假如现在来了一个3位置(1)
自然你要弹出2位置(3),cur=2,结算
L=-1,R=3
a=R-L-1=3-(-1)-1=3
S=a×h=3×3=9
max=9

这个最优解,依然可以被求出来【最优解后续都会出来的】

懂了吧!!!

美滋滋

逻辑你搞清楚了,自然手撕代码问题不大,我们先用纯粹的单调栈解

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

需要一个o(n)的方法,提前把arr所有i位置的LR保存好
返回一个二维数组

info,info[0]是L,info[1]是R

    //单调栈,
    //一个数,栈为空, 直接往栈里面放,来新数,如果比栈顶大,直接放栈里,
    //如果和栈顶值相等,则,位置,放在与栈顶的list中,
    //如果比栈顶值小,则需要结算了,
    //让栈顶弹出的,当前位置i,就是栈顶右边最近的比栈顶小的位置
    //栈顶刚刚压着谁,这个list末尾位置就是栈顶左边最近的最小的位置
    //如果栈顶弹出后没有元素了,则,左边最小的位置就设定为-1

    //当年所有的位置都访问到arr.len后,单独处理栈中的元素,所有的元素右边最小的位置均为-1,左边看情况,和上面一样

    public static int[][] getSubArrayNumWithSingleStack(int[] arr){
        int[][] res = new int[arr.length][2];
        if (arr == null || arr.length == 0) return res;

        Stack<List<Integer>> stack = new Stack<>();//单调栈

        //挨个遍历所有的元素,O(N)复杂度
        for (int i = 0; i < arr.length; i++) {
            //分三种情况,第一种,新位置的数,比栈顶的值小,要结算了
            while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]){
                //栈顶是一个list,你得看值,值就是0位置,无所谓的,末尾也行,都相当的
                //既然新来的值,小,就要结算,弹出
                List<Integer> popIs = stack.pop();//list
                //我栈顶右边是i,左边得看情况,没压就是-1,压了就是list末尾那个值
                int left = stack.isEmpty()? -1 : stack.peek().get(stack.peek().size() - 1);//末尾
                for(Integer j:popIs){
                    res[j][0] = left;//左边最近比栈顶小的位置
                    res[j][1] = i;//右边最近比栈顶小的肯定是i
                }
            }

            //第2种,新位置的数,比栈顶的值 相等,直接放栈顶那个list
            if (!stack.isEmpty() && arr[i] == arr[stack.peek().get(0)]){
                stack.peek().add(i);//直接放栈顶那个list即可
            }
            //第3种,新位置的数,比栈顶的值 大,新生成list,进栈——很重要
            else {
                ArrayList<Integer> list = new ArrayList<>();
                list.add(i);
                stack.push(list);
            }
        }//每一个元素搞定了,i到了arr.len,单独处理剩下栈中的元素

        while (!stack.isEmpty()){
            //右边全-1,左边看情况
            List<Integer> popIs = stack.pop();
            int left = stack.isEmpty()? -1 : stack.peek().get(stack.peek().size() - 1);
            for(Integer j:popIs){
                res[j][0] = left;
                res[j][1] = -1;
            }
        }

        return res;
    }

当时讲单调栈那会,我讲了重复值怎么解决,那就往栈里面放一个列表,相同arri,把下标们都放进list
然后压栈即可

弹出的时候,全部结算,道理一样的

测试:

    public static void test2(){
        int[] arr = {3,2,1,7,0,4,5,6};
        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();
        }
    }

    public static void main(String[] args) {
        test2();
    }

你看看info是啥?

-1 1 
-1 2 
-1 4 
2 4 
-1 -1 
4 -1 
5 -1 
6 -1 

是不是对的
arr = {3,2,1,7,0,4,5,6};每个位置,左边距离它最近的且比自己小的位置L,右边距离它最近的且比自己小的位置R

那会我们讲的时候,认为右边没有就是空,R=-1,现在我们可以认定为R=N位置
在这里插入图片描述

都是对的

拿这单调栈的代码,改编为解决本题的代码

        //单调栈,
        //一个数,栈为空, 直接往栈里面放,来新数,如果比栈顶大,直接放栈里,
        //如果和栈顶值相等,则,位置,放在与栈顶的list中,
        //如果比栈顶值小,则需要结算了,
        //让栈顶弹出的,当前位置i,就是栈顶右边最近的比栈顶小的位置
        //栈顶刚刚压着谁,这个list末尾位置就是栈顶左边最近的最小的位置
        //如果栈顶弹出后没有元素了,则,左边最小的位置就设定为-1

        //当年所有的位置都访问到arr.len后,单独处理栈中的元素,所有的元素右边最小的位置均为-1,左边看情况,和上面一样

        public static int[][] getSubArrayNumWithSingleStack(int[] arr){
            int[][] res = new int[arr.length][2];
            if (arr == null || arr.length == 0) return res;
            int N = arr.length;

            Stack<List<Integer>> stack = new Stack<>();//单调栈

            //挨个遍历所有的元素,O(N)复杂度
            for (int i = 0; i < arr.length; i++) {
                //分三种情况,第一种,新位置的数,比栈顶的值小,要结算了
                while (!stack.isEmpty() && arr[i] < arr[stack.peek().get(0)]){
                    //栈顶是一个list,你得看值,值就是0位置,无所谓的,末尾也行,都相当的
                    //既然新来的值,小,就要结算,弹出
                    List<Integer> popIs = stack.pop();//list
                    //我栈顶右边是i,左边得看情况,没压就是-1,压了就是list末尾那个值
                    int left = stack.isEmpty()? -1 : stack.peek().get(stack.peek().size() - 1);//末尾
                    for(Integer j:popIs){
                        res[j][0] = left;//左边最近比栈顶小的位置
                        res[j][1] = i;//右边最近比栈顶小的肯定是i
                    }
                }

                //第2种,新位置的数,比栈顶的值 相等,直接放栈顶那个list
                if (!stack.isEmpty() && arr[i] == arr[stack.peek().get(0)]){
                    stack.peek().add(i);//直接放栈顶那个list即可
                }
                //第3种,新位置的数,比栈顶的值 大,新生成list,进栈——很重要
                else {
                    ArrayList<Integer> list = new ArrayList<>();
                    list.add(i);
                    stack.push(list);
                }
            }//每一个元素搞定了,i到了arr.len,单独处理剩下栈中的元素

            while (!stack.isEmpty()){
                //右边全-1,左边看情况
                List<Integer> popIs = stack.pop();
                int left = stack.isEmpty()? -1 : stack.peek().get(stack.peek().size() - 1);
                for(Integer j:popIs){
                    res[j][0] = left;
                    res[j][1] = N;
                }
            }

            return res;
        }


        public int largestRectangleArea(int[] heights) {
            if (heights == null || heights.length == 0) return 0;

            int max = 0;
            int N = heights.length;

            int[][] info = getSubArrayNumWithSingleStack(heights);//单调栈拿到info LR保存好

            //遍历arr,求s,更新给max
            for (int i = 0; i < N; i++) {
                //i左边距离它最近的且比自己小的位置L,右边距离它最近的且比自己小的位置R
                int L = info[i][0];
                int R = info[i][1];

                int a = R - L - 1;
                int s = a * heights[i];
                max = Math.max(max, s);
            }

            return max;
        }

和单调栈上面唯一的区别就是栈顶没有了的时候,R=res[j][1] = N;
我们要的是右边越界位置

这样好算a

okay测试:

    public static void test2(){

        Solution solution = new Solution();

//        int[] arr = {3,2,1,7,0,4,5,6};
//
//        int[][] res = solution.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();
//        }

        int[] arr2 = {2,1,5,6,2,3};//LeetCode测试案例
        System.out.println(solution.largestRectangleArea(arr2));
    }

    public static void main(String[] args) {
        test2();
    }

结果10
很OK

LeetCode测试也非常非常OK
在这里插入图片描述
在这里插入图片描述
够牛吧?
速度不够快,是因为


下面咱们手撕我们最开始讲的那种方法,在单调栈操作弹出的时候直接结算当前栈顶cur的面积

不要单独求info
直接在i位置小于等于栈顶时,直接弹出栈顶cur,结算
哪怕遇到重复的arri,没关系,先求次优解,问题不大

        //重复元素不用列表搞,直接弹出,次优解拿住,最优解也不会错过的
        public int largestRectangleArea2(int[] heights) {
            if (heights == null || heights.length == 0) return 0;

            int max = 0;
            int N = heights.length;

            //int[][] info = getSubArrayNumWithSingleStack(heights);//单调栈拿到info LR保存好

            //准备单调栈
            Stack<Integer> stack = new Stack<>();//放arr的下标哦——规则,栈底小,栈顶大

            //遍历arr,压栈,如果遇到更小的弹出cur,求s,更新给max
            for (int i = 0; i < N; i++) {
                while (!stack.isEmpty() && heights[i] <= heights[stack.peek()]){
                    //刚刚来的i,比栈顶要小,或者等于,都要弹出,这样可能是次优解,没关系——结算
                    //i左边距离它最近的且比自己小的位置L,右边距离它最近的且比自己小的位置R
                    int cur = stack.pop();//弹出栈顶
                    int L = stack.empty() ? -1 : stack.peek();//弹出cur之后没压,那就-1,压了,就是它
                    int R = i;//谁让栈顶弹出的?i位置呗

                    int a = R - L - 1;
                    int s = a * heights[cur];//高度就是当前弹出的高度哦,别搞错了
                    max = Math.max(max, s);
                }
                //不进while,那就是正常,要么第一次来,要么就是栈顶还能升序,不管咋样,都要让i进展
                stack.push(i);
                //当arr都放完了,栈还有内容,自然右边就没有数比栈顶小了,R=N
            }

            while (!stack.isEmpty()){
                int cur = stack.pop();//弹出栈顶
                int L = stack.empty() ? -1 : stack.peek();//弹出cur之后没压,那就-1,压了,就是它
                int R = N;//谁让栈顶弹出的?i位置呗
                int a = R - L - 1;
                int s = a * heights[cur];//高度就是当前弹出的高度哦,别搞错了
                max = Math.max(max, s);
            }

            return max;
        }

中途记住了,进栈前,不断检查,需要弹出就弹出,所以while存在了
不管咋样,弹出结算之后,i必须进栈

测试:

    public static void test2(){

        Solution solution = new Solution();

//        int[] arr = {3,2,1,7,0,4,5,6};
//
//        int[][] res = solution.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();
//        }

        int[] arr2 = {2,1,5,6,2,3};//LeetCode测试案例
        System.out.println(solution.largestRectangleArea(arr2));
        System.out.println(solution.largestRectangleArea2(arr2));
    }

    public static void main(String[] args) {
        test2();
    }

结果

10
10

LeetCode测试;
在这里插入图片描述
在这里插入图片描述
很OK吧

就这样
单调栈的妙处,懂?


总结

提示:重要经验:

1)单调栈那个文章,我讲的很细,用list装重复的arri,这样结算的时候,找那个位置就不会出错
2)对于本题,单调栈,直接干,当i位置<=栈顶,立马弹出栈顶cur,结算搞定
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到