leetcode学习笔记:
leetcode做的第一道hard难度的题,找条形统计图中的最大矩形面积。给定一个数组,最直观的做法就是两个循环检索所有的组合,稍微高级点,检索每一个元素时设定左右边界,只计算以它为最小值的时候的矩形面积,这样一来也能确保最大的那种一定被检索,毕竟任何一种情况下,面积的计算都是用最小值乘以当前的子序列长度。不管怎样,这俩复杂度都是O(n^2)。很糟糕的设计。
(1)优化算法一:
复杂度降低到O(nlogn),使用divide and conquer,分治法,递归的思想。
将数组一分为二,两个小数组就是性质相同的子问题,中间那个数就是中间值,总的问题就等于两个子序列中各自能找出的最大的,和跨过中间那个数的最大的,这仨中间的最大值,典型的算法,不做多说。复杂度递推公式为,
T(n)=2T(n/2)+O(n),根据主定理,master theorem很容易算出复杂度。
跨过中间那个数的最大面积,可以从中间那个数向左右检索,找寻以其为最小值的子序列,计算面积,遇到更小的俩边界,取较大的作为最小值,找边界,计算面积,所以相当于从中间值开始,以此降序找最小值对应的面积。然而这里我一开始超时了,因为我在递归的时候,对子序列的中间值也是在整个序列中找以它或者小于它为中心的最大矩阵,这样增加很多不必要的计算。因为,如果子序列的中间值小于母序列的,那么在母序列中一定会计算它的存在,如果大于的话,母序列的中间值就是它的边界,所以不用检索所有的。给出我的c++源码如下:
//跨过这个数最大的矩形,可以是以这个数为最小值,或者是以小于这个的数为最小值。
int largestMid(vector<int>& heights, int begin, int end, int mid) {
int j = mid - 1, k = mid + 1;
int cur_height = heights[mid];
int area = 0;
int maxrect = 0;
while (j<k) //左右检索以当前值为最小值的边界
{
while (j >= begin && heights[j] >= cur_height) j--;
while (k <= end && heights[k] >= cur_height) k++;
area = (k - j - 1)*cur_height;
if (area > maxrect) maxrect = area;
if (j >= begin && k <= end)
cur_height = max(heights[j], heights[k]);
else {
if (j < begin && k > end ) break;
else if (j < begin) cur_height = heights[k];
else if (k > end) cur_height = heights[j];
}
}
return maxrect;
}
// 这个就是递归的主要代码,同一个序列传递左右下标,不用新建空间,也节约时间。
int largestRec(vector<int>& heights, int begin, int end) {
if (begin == end) return heights[begin];
if (end - begin == 1) return max(min(heights[begin], heights[end]) * 2, max(heights[begin], heights[end]));
while (begin < end) {
int mid = begin + (end - begin) / 2;
int leftrect = largestRec(heights, begin, mid - 1);
int rightrect = largestRec(heights, mid + 1, end);
int midrect = largestMid(heights, begin, end, mid);
return max(max(leftrect, rightrect), midrect);
}
return 0;
}
// 这个是leetcode的传递函数,这种函数我们经常用上述函数来简化递归。
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
int maxrect = largestRec(heights, 0, n - 1);
return maxrect;
}
(2)优化算法二:
leetcode里上述算法可以通过。但也可以只线性检索一遍。如果我们能在一遍检索里找到每一个元素作为最小值的左右边界,计算面积,那样确实可以,计算面积我们只需要最小值以及边界范围,所以如何动态有效的保存序列中的序号是值得考虑的。参考了网上的做法,大多采用一个stack存数组里的下标,栈内只保持递增的数组值,一旦遇到值下降的,就证明边界条件完整了,因为每一个的左侧都是左边界,现在右边界出来了,挨个弹出,计算每一个作为最小值的面积。直到栈内保持递增关系,因为此时右边界失效了。继续找。因为我们存的是下标,所以子序列的长度是可以得到保证的,当然,如果新出现的元素把栈内清空了,那只能说明他是当前最小的,计算面积只需要当前值得下标,因为之前的所有数组值都需要被包含。
代码如下:
Java里的:
public static int largestRectangleArea2(int[] heights) {
Stack<Integer> stack = new Stack<Integer>();
int[] h = new int[heights.length+1];
h = Arrays.copyOf(heights, heights.length+1);
int i, t, maxrect=0;
for (i=0; i<h.length; ) {
if (stack.isEmpty() || h[stack.peek()]<=h[i]) {
stack.push(i++);
}
else {
t= stack.pop();
if (stack.isEmpty()) {
maxrect =Math.max(maxrect, h[t]*i);
}
else maxrect =Math.max(maxrect, h[t]*(i-stack.peek()-1));
}
}
return maxrect;
}
C++里的:
int largestRectangleArea2(vector<int>& heights) {
stack<int> h;
int len = heights.size();
if (len == 0) return 0; //leetcode总有这种判空的特例,养成习惯。对输入多判断下。
int i, t, maxrect = 0;
for (i = 0; i < len; ) {
if (h.empty() || heights[h.top()] <= heights[i]) {
h.push(i++); //保持递增序列,直到找到目前最大数的右边界。
} //注意stack栈底永远是目前最小值下标,因为如果不是一定会被最小值作为右边界弹出算面积。
else {
t = h.top();
h.pop();
if (h.empty())
maxrect = max(maxrect, heights[t] * i);//空代表此时弹出的是当前最小值,直接乘当前长度
else {
maxrect = max(maxrect, heights[t] * (i - h.top() - 1));
}//如果非空,左侧为左边界下标,右侧为右边界下标,算一下当前最小值的长度范围求面积
}
}
int temp = 0;
while(i < len + 1) { //对最后push进去的那个数得善后
if (h.empty() || heights[h.top()] <= temp) {
h.push(i++);
}
else {
t = h.top();
h.pop();
if (h.empty())
maxrect = max(maxrect, heights[t] * i);
else {
maxrect = max(maxrect, heights[t] * (i - h.top() - 1));
}
}
}
return maxrect;
}
以上两个代码几乎一样的,唯一的不同就是,Java那个新建的一个数组,原有数组的基础上长度加一,新存储了个0,作为有效右边界。第二个我直接单独把最后一个写出来。代码长度增加了,但感觉不用把原数组复制一遍还是好了些。
合理的使用数据结构和常用算法特别重要,刷题加总结是提高这种问题的解决能力的唯一途径。共勉。