https://oj.leetcode.com/problems/largest-rectangle-in-histogram/
有图,描述自己进链接看。。。
public int largestRectangleArea(int[] height)
这题可以暴力做,也就是二重循环即可。但是这样就失去本题的意义了
所以,我们要考虑一个更巧妙的O(N)的做法。这就要利用到Stack。我先直接给出算法,随后给出代码
算法如下,循环往下走,当符合以下两个条件的时候,压栈:1.栈为空。2.栈顶比当前元素小。压栈的内容是当前的index。
当遇到栈顶比当前元素大的时候,做以下循环,不停pop栈,并且求与前一个bar之间所产生的面积,维护其中最大者。
另外当循环完结之后,若堆栈内还有元素,依旧可以尝试不停pop然后维护面积
public int largestRectangleArea(int[] height) {
Stack<Integer> index_stack = new Stack<Integer>();
int sum = 0;
for(int i = 0; i < height.length; i++){
if(index_stack.isEmpty() || height[i] >= height[index_stack.peek()])
index_stack.push(i);
else{
while(!index_stack.isEmpty() && height[index_stack.peek()] > height[i]){
int index = index_stack.pop();
sum = Math.max(sum, index_stack.isEmpty() ? height[index] * i : height[index] * (i - index_stack.peek() - 1));
}
index_stack.push(i);
}
}
while(!index_stack.isEmpty()){
int index = index_stack.pop();
sum = Math.max(sum, index_stack.isEmpty() ? height[index] * height.length : height[index] * (height.length - index_stack.peek() - 1));
}
return sum;
}
2017-12-12 Updated:
重新回顾这题目的时候,我会发现我自己最不熟悉的题目就是只能死记硬背并无法理解题目算法的题目。这就是其中一题。所以我花了一点时间去理解这一题为什么是要这么做的。那么,我先给出一个比上面那个做法次一些的brute force。
算法的概念是这样的,因为在这个图里面,给定左边界和右边界。正方形的面积是等于左边界和右边界的距离乘以之间最短的那根bar的长度。所以如果我们给定一根bar为中心,找到以这个bar为中心的最左边界和最右边界(边界的定义是不小于当前bar高度的眼神边界,因为一旦超过了边界,出现了比当前bar矮的bar,那么这个bar作为高度的界定点就失效了。)计算面积,我们遍历图里面的所有bar,做这样的边界界定和面积计算,我们总是能够找到最大的面积的。不需要担心在计算较高的bar时因为边界的界定错过了更矮高度的bar而错过面积的计算,因为每一根bar都会有成为一次中心的机会,当更矮的bar做为中心时,它的边界计算总是会覆盖更高的bar。所以这样是可以取到最大的面积的。这种算法的复杂度是o(n^2)。因为每一根bar为中心时,我们都会可能往左往右遍历一个o(n)。所以n根bar的计算就会产生o(n^2)的效率。
下面给出代码:
public int largestRectangleArea(int[] heights) {
int result = 0;
for (int i = 0; i < heights.length; i++) {
result = Math.max(result, bruteForce(heights, i));
}
return result;
}
private int bruteForce(int[] height, int mid) {
int left = mid, right = mid;
while (left >= 0 && height[left] >= height[mid]) left--;
while (right < height.length && height[right] >= height[mid]) right++;
return height[mid] * (right - left - 1);
}
这个代码是过不了leetcode的,因为会有一个非常极端的case出现TLE而无法通过。所以最后的通过率是95/96。leetcode就是那么贱,总会给你一个或者几个极端的case去挑战你的performance下限。
但是最优的算法就是在这个brute force上进化而来。
所以最优的算法实际上就是用o(n)的办法找到每一个midbar所对应的左边界和右边界。换一句话说,就是找到midbar左边第一个比midbar小的数,和右边第一个比midbar小的数。那么回到最优的算法,下面是分析。先贴一段更容易理解的代码:
public int largestRectangleArea(int[] heights) {
Stack<Integer> cacheStack = new Stack<Integer>();
int result = 0;
for (int i = 0; i < heights.length; i++) {
if (cacheStack.isEmpty() || heights[cacheStack.peek()] < heights[i]) {
cacheStack.push(i);
} else {
while(!cacheStack.isEmpty() && heights[cacheStack.peek()] > heights[i]) {
int midBar = cacheStack.pop();
int leftBoundary = cacheStack.isEmpty() ? 0 : cacheStack.peek() + 1;
result = Math.max(result, heights[midBar] * (i - leftBoundary));
}
cacheStack.push(i);
}
}
while(!cacheStack.isEmpty()) {
int midBar = cacheStack.pop();
int leftBoundary = cacheStack.isEmpty() ? 0 : cacheStack.peek() + 1;
result = Math.max(result, heights[midBar] * (heights.length - leftBoundary));
}
return result;
}
先说关键几点。
1. 事实上Stack里面的数字所对应的高度是排好序的。
2. Stack上面的下面的一个元素实际上就是上面的一个元素的左边界。如果这个元素是栈底,表示heights里左边并没有比它小的,0就是它的左边界。如果循环结束Stack里面
还有残留的元素,栈顶必然是最后一个元素,同时也表示,栈中元素的右方,并没有比它小的元素。所以栈中元素的右边界是这个histogram数组的长度。
3. 在else的逻辑里,我们每pop出来一个元素,实际上pop出来的是brute force里每一轮里的midBar,当前的i是右边界。根据2我们可以得知peek()是左边界。
4. 因为每一个元素都会被push一次。根据3,我们可以得知我们已经用了O(N)的方式做到了上述暴力解所做的事情。遍历所有midbar,找到符合条件的左右边界,求出所有midbar对应的正方形中的最大解。
结合算法我们可以得到一定的解析:
1. 算法里,如果当前高度等于栈顶index对应的高度,我们就push,这个做到了上面关键点1和2。
2. 然后如果当前高度小于栈顶index高度,就表示当前所在高度就是栈顶上面所有大于当前栈顶index高度的元素集的右边界。所以此时我们知道了右边界和左边界。我们就把栈顶符合条件的元素都push出来计算一次。
3. 根据1和2,可以保证2的后半句也可以达成。