第十章 单调栈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;
};
改进内容:
-
堆栈边界检查:通过
stack.length来判断栈是否为空。在栈为空的情况下,不能形成左边界,不应该继续计算水量。 -
高度差计算:水的高度应该是当前柱子和栈顶柱子的最小值减去弹出的柱子高度。
-
宽度计算:左右边界之间的距离决定了可以容纳多少水。宽度为当前柱子与栈顶柱子之间的距离
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
解释:
- 当
i = 3时,遇到的柱子高度为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; // 返回最大矩形面积
};
核心思路:
-
初始化:
- 在高度数组的末尾添加一个虚拟的高度
0,保证所有柱子都可以被处理(确保栈在最后一次完全弹出)。 - 栈的初始化为
[-1],其中-1作为栈底,用于方便计算矩形的宽度。
- 在高度数组的末尾添加一个虚拟的高度
-
遍历高度数组:
- 对于每个柱子
heights[i],如果当前高度比栈顶柱子的高度小,则栈顶柱子无法延伸到当前柱子的位置了,因此需要弹出栈顶并计算以栈顶柱子为高度的矩形面积。 - 宽度
w的计算:i - stack[stack.length - 1] - 1,即当前索引i到栈顶下一个元素之间的距离。
- 对于每个柱子
-
栈的使用:
- 栈中存储的始终是递增顺序的柱子的索引,当遇到一个比栈顶小的柱子时,意味着以栈顶柱子为高度的矩形结束,开始计算面积并更新最大值。
-
弹出时的面积计算:
- 每当从栈中弹出元素时,假设这个柱子为高度,宽度为它能向左和向右扩展的最大距离。
示例:
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],弹出高度为6和5的柱子,计算面积5 * 2 = 10【最大面积】
方法二:首尾加0
在原数组首尾加 0,这是一种可以优化处理的技巧,常用于解决类似问题。通过在首尾增加 0,你可以确保所有柱子在遍历中都被弹出处理,避免在最后单独处理栈中剩余的柱子。
为什么要在首尾加 0?
-
自动处理边界:
- 在解决像 柱状图最大矩形面积 这种问题时,栈中的元素代表递增高度的柱子。遍历完数组后,栈中可能仍有一些元素没有被处理。为了避免在遍历完成后需要额外的操作清空栈,可以在原数组的末尾添加
0,这样遍历到最后这个0时,栈中的所有柱子都能被弹出处理。 - 同理,如果在开头加
0,可以避免需要单独处理数组开始处的情况,使得逻辑更加统一。
- 在解决像 柱状图最大矩形面积 这种问题时,栈中的元素代表递增高度的柱子。遍历完数组后,栈中可能仍有一些元素没有被处理。为了避免在遍历完成后需要额外的操作清空栈,可以在原数组的末尾添加
-
简化边界条件判断:
- 在处理首尾边界时,不必进行额外的判断,通过在数组首尾加上
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; // 返回最大矩形面积
};
解释代码:
-
在数组首尾添加
0:- 使用
heights = [0, ...heights, 0];来在heights数组的首尾各添加一个0,方便处理边界的情况。
- 使用
-
遍历数组:
- 从第一个元素开始遍历
heights,每当遇到当前柱子heights[i]小于栈顶柱子heights[stack[stack.length - 1]]时,开始弹出栈顶并计算矩形面积。
- 从第一个元素开始遍历
-
面积计算:
- 栈弹出后,以弹出的高度为高,宽度
w为当前索引i到栈顶下一个元素之间的距离i - stack[stack.length - 1] - 1。
- 栈弹出后,以弹出的高度为高,宽度
-
返回结果:
- 遍历完成后,所有可能的矩形面积都已被处理,返回最大的矩形面积。
优点:
- 代码简化:在首尾加
0后,无需在最后额外处理栈中的元素,也不需要在开头做特殊处理。 - 边界处理一致性:在处理边界时,与处理中间元素的逻辑完全相同,代码可读性和维护性更好。
示例:
let heights = [2,1,5,6,2,3];
console.log(largestRectangleArea(heights));
// 输出: 10