【算法60天:Day60】第十章单调栈 柱状图中最大矩形(84)

91 阅读2分钟

题目一:

image.png

思路

本题和42. 接雨水 (opens new window),是遥相呼应的两道题目,建议都要仔细做一做,原理上有很多相同的地方,但细节上又有差异,更可以加深对单调栈的理解!

其实这两道题目先做那一道都可以,但先写的42.接雨水的题解,所以如果没做过接雨水的话,建议先做一做接雨水,可以参考题解:42. 接雨水(opens new window)

我们先来看一下双指针的解法:

#双指针解法

var largestRectangleArea = function(heights) {
    let sum = 0;
    for (let i = 0; i < heights.length; i++) {
        let left = i;
        let right = i;
        for (; left >= 0; left--) {
            if (heights[left] < heights[i]) break;
        }
        for (; right < heights.length; right++) {
            if (heights[right] < heights[i]) break;
        }
        let w = right - left - 1;
        let h = heights[i];
        sum = Math.max(sum, w * h);
    }
    return sum;
};

如上代码并不能通过leetcode,超时了,因为时间复杂度是O(n2)O(n^2)

#动态规划

本题动态规划的写法整体思路和42. 接雨水 (opens new window)是一致的,但要比42. 接雨水 (opens new window)难一些。

难就难在本题要记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。

所以需要循环查找,也就是下面在寻找的过程中使用了while,详细请看下面注释,整理思路在题解:42. 接雨水 (opens new window)中已经介绍了。

//动态规划 js中运行速度最快
var largestRectangleArea = function(heights) {
    const len = heights.length;
    const minLeftIndex = new Array(len);
    const maxRigthIndex = new Array(len);
    // 记录每个柱子 左边第一个小于该柱子的下标
    minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环
    for(let i = 1; i < len; i++) {
        let t = i - 1;
        // 这里不是用if,而是不断向左寻找的过程
        while(t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t];
        minLeftIndex[i] = t;
    }
    // 记录每个柱子 右边第一个小于该柱子的下标
    maxRigthIndex[len - 1] = len; // 注意这里初始化,防止下面while死循环
    for(let i = len - 2; i >= 0; i--){
        let t = i + 1;
        // 这里不是用if,而是不断向右寻找的过程
        while(t < len && heights[t] >= heights[i]) t = maxRigthIndex[t];
        maxRigthIndex[i] = t;
    }
    // 求和
    let maxArea = 0;
    for(let i = 0; i < len; i++){
        let sum = heights[i] * (maxRigthIndex[i] - minLeftIndex[i] - 1);
        maxArea = Math.max(maxArea , sum);
    }
    return maxArea;
};

#单调栈

本地单调栈的解法和接雨水的题目是遥相呼应的。

为什么这么说呢,42. 接雨水 (opens new window)是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子。

这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小

在题解42. 接雨水 (opens new window)中我讲解了接雨水的单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。

那么因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序!

我来举一个例子,如图:

84.柱状图中最大的矩形

只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。

所以本题单调栈的顺序正好与接雨水反过来。

此时大家应该可以发现其实就是栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度

理解这一点,对单调栈就掌握的比较到位了。

除了栈内元素顺序和接雨水不同,剩下的逻辑就都差不多了,在题解42. 接雨水 (opens new window)我已经对单调栈的各个方面做了详细讲解,这里就不赘述了。

剩下就是分析清楚如下三种情况:

  • 情况一:当前遍历的元素 heights[i] 小于栈顶元素 heights[stack[stack.length-1]] 的情况
  • 情况二:当前遍历的元素 heights[i] 等于栈顶元素 heights[stack[stack.length-1]] 的情况
  • 情况三:当前遍历的元素 heights[i] 大于栈顶元素 heights[stack[stack.length-1]] 的情况

JS代码如下:

//单调栈
var largestRectangleArea = function(heights) {
    let maxArea = 0;
    const stack = [];
    heights = [0,...heights,0]; // 数组头部加入元素0 数组尾部加入元素0
    for(let i = 0; i < heights.length; i++){
        if(heights[i] > heights[stack[stack.length-1]]){ // 情况三
            stack.push(i);
        } else if(heights[i] === heights[stack[stack.length-1]]){ // 情况二
            stack.pop(); // 这个可以加,可以不加,效果一样,思路不同
            stack.push(i);
        } else { // 情况一
            while(heights[i] < heights[stack[stack.length-1]]){// 当前bar比栈顶bar矮
                const stackTopIndex = stack.pop();// 栈顶元素出栈,并保存栈顶bar的索引
                let w = i - stack[stack.length -1] - 1;
                let h = heights[stackTopIndex]
                // 计算面积,并取最大面积
                maxArea = Math.max(maxArea, w * h);
            }
            stack.push(i);// 当前bar比栈顶bar高了,入栈
        }
    }
    return maxArea;
};

代码精简之后:

//单调栈 简洁
var largestRectangleArea = function(heights) {
    let maxArea = 0;
    const stack = [];
    heights = [0,...heights,0]; // 数组头部加入元素0 数组尾部加入元素0
    for(let i = 0; i < heights.length; i++){ // 只用考虑情况一 当前遍历的元素heights[i]小于栈顶元素heights[stack[stack.length-1]]]的情况
        while(heights[i] < heights[stack[stack.length-1]]){// 当前bar比栈顶bar矮
            const stackTopIndex = stack.pop();// 栈顶元素出栈,并保存栈顶bar的索引
            let w = i - stack[stack.length -1] - 1;
            let h = heights[stackTopIndex]
            // 计算面积,并取最大面积
            maxArea = Math.max(maxArea, w * h);
        }
        stack.push(i);// 当前bar比栈顶bar高了,入栈
    }
    return maxArea;
};

这里我依然建议大家按部就班把版本一写出来,把情况一二三分析清楚,然后在精简代码到版本二。 直接看版本二容易忽略细节!