Hard | LeetCode 42. 接雨水 | 单调栈 | 双指针
42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
提示:
n == height.length
0 <= n <= 3 * 104
0 <= height[i] <= 105
解题思路
方法一:暴力
对于数组中的每个元素,我们找出下雨后水能达到的最高位置,等于两边最大高度的较小值减去当前高度的值。
时间复杂度: O(N ^ 2) 空间复杂度: O(1)
public int trap(int[] height) {
int ans = 0;
int size = height.length;
for (int i = 1; i < size - 1; i++) {
int max_left = 0, max_right = 0;
// 找左边的最大值
for (int j = i; j >= 0; j--) {
max_left = Math.max(max_left, height[j]);
}
// 找右边的最大值
for (int j = i; j < size; j++) { //Search the right part for max bar size
max_right = Math.max(max_right, height[j]);
}
// 当前柱子能接的雨水量就是(左边最大值, 右边最大值)的较小者 减去 当前柱子的高度
ans += Math.min(max_left, max_right) - height[i];
}
return ans;
}
方法二:更好的暴力
在方法一的基础上, 利用动态规划优化左边的最大值和右边的最大值的计算方法。
时间复杂度: O(N ^ 2) 空间复杂度: O(N)
public int trap(int[] height) {
if (height == null || height.length == 0)
return 0;
int ans = 0;
int size = height.length;
// 先使用动态规划的方法, 使用两个辅助数组分别记录左右两边的最大值
int[] left_max = new int[size];
int[] right_max = new int[size];
left_max[0] = height[0];
for (int i = 1; i < size; i++) {
left_max[i] = Math.max(height[i], left_max[i - 1]);
}
right_max[size - 1] = height[size - 1];
for (int i = size - 2; i >= 0; i--) {
right_max[i] = Math.max(height[i], right_max[i + 1]);
}
// 当前柱子能接的雨水量就是(左边最大值, 右边最大值)的较小者 减去 当前柱子的高度
for (int i = 1; i < size - 1; i++) {
ans += Math.min(left_max[i], right_max[i]) - height[i];
}
return ans;
}
方法三: 单调递减栈
只有在高度下降的时候形成一个低洼, 这样等到高度上升的时候, 形成一个凹槽。然后将栈中比当前元素小的值出栈, 并且这些出栈的储水量是已经可以明确了, 就是这个元素的值。因为是较小者决定了能够储蓄水的高度。
public int trap(int[] height) {
int ans = 0, current = 0;
Deque<Integer> stack = new LinkedList<Integer>();
while (current < height.length) {
// 单调栈增加元素, 首先将栈顶比当前数字小的元素全部出栈
while (!stack.isEmpty() && height[current] > height[stack.peek()]) {
int top = stack.pop();
if (stack.isEmpty())
break;
// 计算当前的柱子, 到出栈元素 柱子的横坐标之差
int distance = current - stack.peek() - 1;
int bounded_height = Math.min(height[current], height[stack.peek()]) - height[top];
ans += distance * bounded_height;
}
stack.push(current++);
}
return ans;
}
方法四: 双指针法
所以我们可以认为如果一端有更高的条形块(例如右端),积水的高度依赖于当前方向的高度(从左到右)。 当我们发现另一侧(右侧)的条形块高度不是最高的,我们则开始从相反的方向遍历(从右到左)。 在遍历时维护 left_max 和 right_max ,但是我们现在可以使用两个指针交替进行,实现 1 次遍历即可完成。
双指针移动时, 每次一定都是指针小的那个移动, 因为指针大的移动没有意义了。
如果指针移到了更大的位置, 宽度减小了, 高度还是不变。移到了更小的位置, 则宽度减小了, 高度也减小了, 得到了一个更小的结果, 没有意义。所以每次都是指针小的指向对方方向移动。
时间复杂度: O(N) 空间复杂度: O(1)
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int ans = 0;
int left_max = 0, right_max = 0;
while (left < right) {
// left指针值小于right指针值, 说明当前应该处理左指针, 因为此时存水的高度由左指针决定
if (height[left] < height[right]) {
if (height[left] >= left_max) {
// 左边没有比当前更大的值, 说明此left不是一块洼地, 不能存水
left_max = height[left];
} else {
// height[left] < left_max 说明此地方是一块洼地, 可以存水
// 并且存水的高度就是洼地的高度
ans += (left_max - height[left]);
}
++left;
} else {
// 此else分支说明当前存水的高度由右指针决定
if (height[right] >= right_max) {
// 右边没有比当前更大的值,说明当前right位置, 不能存水
right_max = height[right];
} else {
ans += (right_max - height[right]);
}
--right;
}
}
return ans;
}