379,柱状图中最大的矩形(难)_编程开发

Remember, quitters never win...and winners never quit.

记住,放弃者难以成功,成功者决不放弃。

 

问题描述

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

 

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

379,柱状图中最大的矩形(难)_编程开发_02

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

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

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

 

示例:

 

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

输出: 10

问题分析

01暴力求解

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

 

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

 

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

 

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

1public int largestRectangleArea(int[] heights) {
2 int length = heights.length;
3 int area = 0;
4 // 枚举左边界
5 for (int left = 0; left < length; ++left) {
6 int minHeight = Integer.MAX_VALUE;
7 // 枚举右边界
8 for (int right = left; right < length; ++right) {
9 // 确定高度,我们要最小的高度
10 minHeight = Math.min(minHeight, heights[right]);
11 // 计算面积,我们要保留计算过的最大的面积
12 area = Math.max(area, (right - left + 1) * minHeight);
13 }
14 }
15 return area;
16}

暴力解法的另一种写法

1public int largestRectangleArea(int[] heights) {
2 int area = 0, length = heights.length;
3 // 遍历每个柱子,以当前柱子的高度作为矩形的高 h,
4 // 从当前柱子向左右遍历,找到矩形的宽度 w。
5 for (int i = 0; i < length; i++) {
6 int w = 1, h = heights[i], j = i;
7 //往左边找
8 while (--j >= 0 && heights[j] >= h) {
9 w++;
10 }
11 j = i;
12 //往右边找
13 while (++j < length && heights[j] >= h) {
14 w++;
15 }
16 //记录最大面积
17 area = Math.max(area, w * h);
18 }
19 return area;
20}

 

02使用栈求解

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

379,柱状图中最大的矩形(难)_编程开发_04

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

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

 

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

 

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

 

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

 

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

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

379,柱状图中最大的矩形(难)_编程开发_07

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

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

379,柱状图中最大的矩形(难)_编程开发_10

379,柱状图中最大的矩形(难)_编程开发_11

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

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

379,柱状图中最大的矩形(难)_编程开发_14

03使用栈求解代码

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

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

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

 

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 }

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

1public static int largestRectangleArea(int[] height) {
2 if (height == null || height.length == 0) {
3 return 0;
4 }
5 //存放左边比它小的下标
6 int[] leftLess = new int[height.length];
7 //存放右边比它小的下标
8 int[] rightLess = new int[height.length];
9 rightLess[height.length - 1] = height.length;
10 leftLess[0] = -1;
11
12 //计算每个柱子左边比它小的柱子的下标
13 for (int i = 1; i < height.length; i++) {
14 int p = i - 1;
15 while (p >= 0 && height[p] >= height[i]) {
16 p = leftLess[p];
17 }
18 leftLess[i] = p;
19 }
20 //计算每个柱子右边比它小的柱子的下标
21 for (int i = height.length - 2; i >= 0; i--) {
22 int p = i + 1;
23 while (p < height.length && height[p] >= height[i]) {
24 p = rightLess[p];
25 }
26 rightLess[i] = p;
27 }
28 int maxArea = 0;
29 //以每个柱子的高度为矩形的高,计算矩形的面积。
30 for (int i = 0; i < height.length; i++) {
31 maxArea = Math.max(maxArea, height[i] * (rightLess[i] - leftLess[i] - 1));
32 }
33 return maxArea;
34}

 

05总结

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

 

 

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

长按上图,识别图中二维码之后即可关注。