379,柱状图中最大的矩形(难)

想了解更多数据结构以及算法题,可以关注微信公众号“数据结构和算法”,每天一题为你精彩解答。也可以扫描下面的二维码关注
在这里插入图片描述

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


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

在这里插入图片描述

以上是柱状图的示例,其中每个柱子的宽度为 1,给定的高度为 [2,1,5,6,2,3]。

在这里插入图片描述

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。


示例:

输入: [2,1,5,6,2,3]

输出: 10


01
暴力求解

最简单的方式就是暴力求解,我们都知道暴力求解的效率很差,但不妨碍我们做出来。暴力求解有两种方式。


一种是从左边确定一根柱子,然后从左往右扫描,确定以当前柱子的高为最大高度所围成的最大矩形(这个矩形的高度不能超过当前柱子的高度),记录下最大面积。

还一种是确定一根柱子以后分别从他的前后两个方向扫描,确定以当前柱子高度为矩形的高所围成的最大矩形(这个矩形的高度就是当前这个柱子的高度),记录下最大面积。


我们来分别看下这两种写法的代码

public int largestRectangleArea(int[] heights) {
    int length = heights.length;
    int area = 0;
    // 枚举左边界
    for (int left = 0; left < length; ++left) {
        int minHeight = Integer.MAX_VALUE;
        // 枚举右边界
        for (int right = left; right < length; ++right) {
            // 确定高度,我们要最小的高度
            minHeight = Math.min(minHeight, heights[right]);
            // 计算面积,我们要保留计算过的最大的面积
            area = Math.max(area, (right - left + 1) * minHeight);
        }
    }
    return area;
}

暴力解法的另一种写法

public int largestRectangleArea(int[] heights) {
    int area = 0, length = heights.length;
    // 遍历每个柱子,以当前柱子的高度作为矩形的高 h,
    // 从当前柱子向左右遍历,找到矩形的宽度 w。
    for (int i = 0; i < length; i++) {
        int w = 1, h = heights[i], j = i;
        //往左边找
        while (--j >= 0 && heights[j] >= h) {
            w++;
        }
        j = i;
        //往右边找
        while (++j < length && heights[j] >= h) {
            w++;
        }
        //记录最大面积
        area = Math.max(area, w * h);
    }
    return area;
}

02
使用栈求解

我们看一下暴力求解的第二种方式,他是每遍历一根柱子就会往左和往右查找,直到找到比他小的为止,然后以当前柱子的高度为矩形的高,以不低于当前柱子的数量(必须是和当前柱子挨着的)为矩形的宽来计算矩形的面积,我们就用上面的示例以当前高度为5的柱子为例来画个图看一下。

在这里插入图片描述
在这里插入图片描述


看明白了上面的分析,我们是不是会有点启发,我们如果以当前柱子的高度为矩形的高,我们只需要往左和往右找到小于当前的柱子,就可以确定矩形的宽度。知道宽和高面积自然就求出来了。


但是矩形的宽度怎么求呢,我们这里并不是直接求,我们要维护一个递增的栈(从栈底到栈顶的元素所对应柱子的高度是递增的),注意栈中存放的是柱子的下标,不是柱子的高度


我们每遍历一个柱子的时候如果当前柱子i的值大于等于栈顶元素对应柱子的高度,我们就把当前柱子的下标压入到栈顶中。

如果当前柱子i的值小于栈顶元素柱子k的高度,说明栈顶元素对应的柱子k遇到了右边比它小的柱子,我们只需要弹出栈顶柱子k。那么怎么确定柱子k他左边比它小的柱子呢,很明显因为栈从栈底到栈顶是递增的,柱子k已经出栈了,现在栈顶元素w对应柱子的高度就是柱子k遇到的左边比他小的值(有可能这时候栈顶元素w对应柱子的高度和柱子k对应的高度一样大,但没关系,因为下一步我们还会在继续计算)。 根据上面的暴力求解,我们知道一个柱子左边和右边比它小的值,就可以以当前柱子的高度为矩形的高,计算出矩形的面积。然后我们在用栈顶元素w对应的值和柱子i对应的值比较,重复上面的步骤……直到柱子i对应的值大于栈顶元素对应的值(或栈为空)为止。(注意这里的比较是栈中元素对应柱子高度的比较,不是栈中元素的比较)


上面的解说比较绕,看不明白可以多读几遍,我们来画个图看一下

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

03
使用栈求解代码

public int largestRectangleArea(int[] heights) {
    int length = heights.length;
    Stack<Integer> stack = new Stack<>();
    int maxArea = 0;
    for (int i = 0; i <= length; i++) {
        int h = (i == length ? 0 : heights[i]);
        //如果栈是空的,或者当前柱子的高度大于等于栈顶元素所对应柱子的高度,
        //直接把当前元素压栈
        if (stack.isEmpty() || h >= heights[stack.peek()]) {
            stack.push(i);
        } else {
            int top = stack.pop();
            int area = heights[top] * (stack.isEmpty() ? i : i - 1 - stack.peek());
            maxArea = Math.max(maxArea, area);
            i--;
        }
    }
    return maxArea;
}

这题标注是难,确实有一定的难度,如果上面图看懂了,上面代码也就不难理解了。其实我们还可以换种思路,在柱状图的最左边和最右边分别增加一个高度为0的柱子,这样代码写起来也比较容易理解,图就不再画了,代码中有详细的注释,我们直接看代码

public int largestRectangleArea(int[] heights) {
    //申请一个比heights长度大2的临时数组
    int[] tmp = new int[heights.length + 2];
    //把数组heights的值复制到数组tmp中,并且tmpd第一个元素
    // 和最后一个元素都是0,表示高度为0的柱子
    System.arraycopy(heights, 0, tmp, 1, heights.length);
    Stack<Integer> stack = new Stack<>();//栈
    int maxArea = 0;
    for (int i = 0; i < tmp.length; i++) {
        //如果当前值tem[i]比栈顶元素对应的柱子高度小,说明栈顶元素的柱子遇到
        // 了右边比它小的柱子。那么他左边比它小的就是栈顶元素所对应的柱子高度
        // (因为栈中元素从栈底到栈顶对应柱子的高度是递增的),知道左右两边比
        // 它小的就可以确定矩形的面积了,但这个矩形不一定是最大的,所以我们要保存下来
        while (!stack.isEmpty() && tmp[i] < tmp[stack.peek()]) {
            int h = tmp[stack.pop()];
            //计算矩形的面积
            int area = (i - 1 - stack.peek()) * h;
            //哪个大留哪个
            maxArea = Math.max(maxArea, area);
        }
        //注意这里入栈的是柱子的下标,不是柱子的高度
        stack.push(i);
    }
    return maxArea;
}

04
通过两边的临界值求解

根据上面的分析,我们知道对于第i根柱子所围成的最大矩形是

s=(right-left-1)*height[i]

其中right是右边比它小的柱子的下标,left是左边比它小的柱子的下标,height[i]是当前柱子的高度。


如果我们知道每根柱子左右两边比它小的值,我们就可以求出最大面积

1    int maxArea = 0;
2    for (int i = 0; i < height.length; i++) {
3        maxArea = Math.max(maxArea, height[i] * (rightLess[i] - leftLess[i] - 1));
4    }

但问题是我们怎么求出左右两边比它小的值呢?比如我们想求左边比它小的值,我们可以这样来计算

1    for (int i = 1; i < height.length; i++) {
2        int p = i - 1;
3        while (p >= 0 && height[p] >= height[i]) {
4            p--;
5        }
6        leftLess[i] = p;
7    }

代码很简单,就是从他的左边挨着的那个一直往左找,直到找到为止。如果没找到p就会为-1,比如一直递减的柱子每一个p都是-1,-1符合上面的公式。同理右边的也一样。

但我们看到上面的查找效率真的不是很高,实际上代码我们还可以再优化一下,如果左边的柱子i比当前柱子k高,那么柱子i左边比柱子i高的肯定也比当前柱子k高,这种我们就不需要在找了,我们要找柱子i左边比柱子i矮的柱子再和当前柱子k对比,我们来看下

1    for (int i = 1; i < height.length; i++) {
2        int p = i - 1;
3        while (p >= 0 && height[p] >= height[i]) {
4            p = leftLess[p];
5        }
6        leftLess[i] = p;
7    }

看明白了上面的分析,代码就容易多了,我们再来看下

public static int largestRectangleArea(int[] height) {
    if (height == null || height.length == 0) {
        return 0;
    }
    //存放左边比它小的下标
    int[] leftLess = new int[height.length];
    //存放右边比它小的下标
    int[] rightLess = new int[height.length];
    rightLess[height.length - 1] = height.length;
    leftLess[0] = -1;

    //计算每个柱子左边比它小的柱子的下标
    for (int i = 1; i < height.length; i++) {
        int p = i - 1;
        while (p >= 0 && height[p] >= height[i]) {
            p = leftLess[p];
        }
        leftLess[i] = p;
    }
    //计算每个柱子右边比它小的柱子的下标
    for (int i = height.length - 2; i >= 0; i--) {
        int p = i + 1;
        while (p < height.length && height[p] >= height[i]) {
            p = rightLess[p];
        }
        rightLess[i] = p;
    }
    int maxArea = 0;
    //以每个柱子的高度为矩形的高,计算矩形的面积。
    for (int i = 0; i < height.length; i++) {
        maxArea = Math.max(maxArea, height[i] * (rightLess[i] - leftLess[i] - 1));
    }
    return maxArea;
}

05
总结

这题如果单从暴力破解的方式上来看不是很难,但我们都知道暴力二字是什么意思,在面试中暴力求解往往不占优势。如果不使用暴力破解这题还是有一定的难度的。


在这里插入图片描述

posted @ 2020-09-24 22:23  数据结构和算法  阅读(95)  评论(0编辑  收藏  举报