DAY47

60 阅读7分钟

第十章 单调栈part02

42. 接雨水

接雨水这道题目是 面试中特别高频的一道题,也是单调栈 应用的题目,大家好好做做。

建议是掌握 双指针 和单调栈,因为在面试中 写出单调栈可能 有点难度,但双指针思路更直接一些。

在时间紧张的情况有,能写出双指针法也是不错的,然后可以和面试官在慢慢讨论如何优化。

programmercarl.com/0042.%E6%8E…

通过栈存储可能会形成“水槽”的索引,并在遇到高柱子时计算可以储存的水量。

注意⚠️:

索引越界:在 Math.min(height[i], height[stack[stack.length - 1]]) 中,当栈中只剩一个元素时,会导致越界。因此我们需要在进入计算之前确保栈里至少有两个元素。

代码:

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function (height) {
    let stack = [];  // 栈用于存储柱子的索引
    let res = 0;     // 结果:储存的水量

    for (let i = 0; i < height.length; i++) {
        // 如果当前高度比栈顶高度高,意味着形成了低洼,可以存水
        while (stack.length && height[i] > height[stack[stack.length - 1]]) {
            let cur = stack.pop();  // 弹出当前柱子

            // 如果栈为空,说明没有左边界,不能形成水槽
            if (!stack.length) break;

            // 计算左右边界的高度差
            let h = Math.min(height[i], height[stack[stack.length - 1]]) - height[cur];
            // 计算宽度:两个边界之间的距离
            let width = i - stack[stack.length - 1] - 1;
            res += h * width;  // 储存的水量为高度差乘以宽度
        }

        stack.push(i);  // 当前柱子入栈
    }

    return res;
};

改进内容:

  1. 堆栈边界检查:通过 stack.length 来判断栈是否为空。在栈为空的情况下,不能形成左边界,不应该继续计算水量。

  2. 高度差计算:水的高度应该是当前柱子和栈顶柱子的最小值减去弹出的柱子高度。

  3. 宽度计算:左右边界之间的距离决定了可以容纳多少水。宽度为当前柱子与栈顶柱子之间的距离 i - stack[stack.length - 1] - 1

示例:

let height = [0,1,0,2,1,0,1,3,2,1,2,1];
console.log(trap(height)); 
// 输出: 6

解释:

  1. i = 3 时,遇到的柱子高度为 2,比栈中上一个柱子高,因此弹出栈顶柱子,计算其存储的水量。
  2. 继续向后遍历,遇到更高的柱子时继续弹出,直到无法形成低洼为止。

通过使用单调栈的方法,可以高效地解决接雨水的问题,确保每个柱子只被遍历一次,从而实现时间复杂度为 O(n) 的解法。

84.  柱状图中最大的矩形

有了之前单调栈的铺垫,这道题目就不难了。

programmercarl.com/0084.%E6%9F…

方法一

通过单调栈来解决,时间复杂度为 O(n),该方法的核心思想是通过栈来维护递增的柱子索引,当遇到当前柱子高度小于栈顶柱子的高度时,开始计算以栈顶柱子为高度的矩形面积。下面解释代码的细节和工作流程:

代码详细解释:

/**
 * @param {number[]} heights
 * @return {number}
 */
var largestRectangleArea = function(heights) {
    heights.push(0); // 添加一个虚拟高度0,确保最后处理完所有柱子
    let stack = [-1]; // 初始化栈,栈底为-1,方便计算宽度
    let res = 0; // 用于存储最大矩形面积

    for (let i = 0; i < heights.length; i++) {
        // 当当前高度小于栈顶柱子的高度时,弹出栈顶元素并计算面积
        while (stack.length > 1 && heights[i] < heights[stack[stack.length - 1]]) {
            let h = heights[stack.pop()]; // 弹出栈顶元素的高度
            let w = i - stack[stack.length - 1] - 1; // 计算宽度,当前索引减去栈顶下一个元素的索引减1
            res = Math.max(res, h * w); // 更新最大矩形面积
        }
        stack.push(i); // 将当前索引压入栈
    }

    return res; // 返回最大矩形面积
};

核心思路:

  1. 初始化

    • 在高度数组的末尾添加一个虚拟的高度 0,保证所有柱子都可以被处理(确保栈在最后一次完全弹出)。
    • 栈的初始化为 [-1],其中 -1 作为栈底,用于方便计算矩形的宽度。
  2. 遍历高度数组

    • 对于每个柱子 heights[i],如果当前高度比栈顶柱子的高度小,则栈顶柱子无法延伸到当前柱子的位置了,因此需要弹出栈顶并计算以栈顶柱子为高度的矩形面积。
    • 宽度 w 的计算:i - stack[stack.length - 1] - 1,即当前索引 i 到栈顶下一个元素之间的距离。
  3. 栈的使用

    • 栈中存储的始终是递增顺序的柱子的索引,当遇到一个比栈顶小的柱子时,意味着以栈顶柱子为高度的矩形结束,开始计算面积并更新最大值。
  4. 弹出时的面积计算

    • 每当从栈中弹出元素时,假设这个柱子为高度,宽度为它能向左和向右扩展的最大距离。

示例:

let heights = [2,1,5,6,2,3];
console.log(largestRectangleArea(heights)); 
// 输出: 10

栈变化过程:

  • 遇到 [2],压入栈:[-1, 0]
  • 遇到 [1]heights[1] < heights[0],弹出高度为 2 的柱子,计算面积 2 * 1 = 2
  • 遇到 [5, 6],依次压入栈:[-1, 1, 2, 3]
  • 遇到 [2]heights[4] < heights[3],弹出高度为 65 的柱子,计算面积 5 * 2 = 10【最大面积】

方法二:首尾加0

在原数组首尾加 0,这是一种可以优化处理的技巧,常用于解决类似问题。通过在首尾增加 0,你可以确保所有柱子在遍历中都被弹出处理,避免在最后单独处理栈中剩余的柱子。

为什么要在首尾加 0

  1. 自动处理边界

    • 在解决像 柱状图最大矩形面积 这种问题时,栈中的元素代表递增高度的柱子。遍历完数组后,栈中可能仍有一些元素没有被处理。为了避免在遍历完成后需要额外的操作清空栈,可以在原数组的末尾添加 0,这样遍历到最后这个 0 时,栈中的所有柱子都能被弹出处理。
    • 同理,如果在开头加 0,可以避免需要单独处理数组开始处的情况,使得逻辑更加统一。
  2. 简化边界条件判断

    • 在处理首尾边界时,不必进行额外的判断,通过在数组首尾加上 0,可以让算法在处理首尾时与处理中间元素时一致,不需要专门的判断逻辑。

如何在首尾加 0 实现:

/**
 * @param {number[]} heights
 * @return {number}
 */
var largestRectangleArea = function(heights) {
    // 在首尾加上0,确保边界条件都能统一处理
    heights = [0, ...heights, 0];
    let stack = []; // 初始化一个空栈
    let res = 0; // 存储最大矩形面积

    for (let i = 0; i < heights.length; i++) {
        // 当当前高度小于栈顶的高度时,处理栈顶柱子的面积
        while (stack.length && heights[i] < heights[stack[stack.length - 1]]) {
            let h = heights[stack.pop()]; // 弹出栈顶高度
            let w = i - stack[stack.length - 1] - 1; // 计算宽度
            res = Math.max(res, h * w); // 更新最大矩形面积
        }
        stack.push(i); // 将当前索引压入栈
    }

    return res; // 返回最大矩形面积
};

解释代码:

  1. 在数组首尾添加 0

    • 使用 heights = [0, ...heights, 0]; 来在 heights 数组的首尾各添加一个 0,方便处理边界的情况。
  2. 遍历数组

    • 从第一个元素开始遍历 heights,每当遇到当前柱子 heights[i] 小于栈顶柱子 heights[stack[stack.length - 1]] 时,开始弹出栈顶并计算矩形面积。
  3. 面积计算

    • 栈弹出后,以弹出的高度为高,宽度 w 为当前索引 i 到栈顶下一个元素之间的距离 i - stack[stack.length - 1] - 1
  4. 返回结果

    • 遍历完成后,所有可能的矩形面积都已被处理,返回最大的矩形面积。

优点:

  • 代码简化:在首尾加 0 后,无需在最后额外处理栈中的元素,也不需要在开头做特殊处理。
  • 边界处理一致性:在处理边界时,与处理中间元素的逻辑完全相同,代码可读性和维护性更好。

示例:

let heights = [2,1,5,6,2,3];
console.log(largestRectangleArea(heights)); 
// 输出: 10