LeetCode 42:接雨水——从当前位置推导左右两侧最大高度
一、题目描述
今天做的是 LeetCode 42:接雨水。
题目给定一个数组:

height = [0,1,0,2,1,0,1,3,2,1,2,1]
数组中的每个数字表示柱子的高度。
下雨之后,柱子之间可以存储一定的雨水,需要计算最终能够接到的雨水总量。
这道题最开始看起来比较直观,但真正写代码时,需要解决一个问题:
对于每一个位置,到底能够接多少水?
二、先考虑一个位置能够接多少水
假设现在看第 i 个柱子:
左边最高柱子 右边最高柱子
↓ ↓
█ █
█ ↓ █
█ █ █
这个位置最终能装多少水,取决于左右两边较矮的那一堵墙。
所以可以得到:
当前位置水量
=
min(左边最高高度, 右边最高高度)
-
当前柱子高度
也就是:
Math.min(max_left[i], max_right[i]) - height[i]
这里的关键就是:
我们需要知道每个位置左边的最大高度,以及右边的最大高度。
三、第一步:记录每个位置左边的最大高度
我定义:
int[] max_left = new int[len];
其中:
max_left[i]
表示:
从数组开头到
i位置为止,出现过的最大高度。
例如:
height =
[0, 1, 0, 2, 1]
那么:
max_left =
[0, 1, 1, 2, 2]
计算方式非常简单:
max_left[0] = height[0];
for(int i = 1; i < len; i++){
max_left[i] = Math.max(height[i], max_left[i - 1]);
}
例如计算 max_left[3]:
height[3] = 2
max_left[2] = 1
所以:
max_left[3] = max(2, 1)
= 2
这样遍历一次,就可以提前把每个位置左侧的最高柱子记录下来。
四、第二步:记录每个位置右边的最大高度
同样的道理,再定义:
int[] max_right = new int[len];
其中:
max_right[i]
表示:
从
i位置到数组末尾为止,出现过的最大高度。
这一次需要从右向左遍历:
max_right[len - 1] = height[len - 1];
for(int i = len - 2; i >= 0; i--){
max_right[i] = Math.max(height[i], max_right[i + 1]);
}
例如:
height =
[0,1,0,2,1]
计算得到:
max_right =
[2,2,2,2,1]
现在,对于每一个位置:
左边最高值
+
右边最高值
+
当前高度
我们都已经可以快速得到。
五、第三步:计算每个位置能够接多少水
有了:
max_left[i]
max_right[i]
那么当前位置的水量就是:
Math.min(max_left[i], max_right[i]) - height[i]
为什么是 min?
因为如果左边最高柱子是:
5
右边最高柱子是:
3
那么水最多只能到:
3
的位置。
如果水超过右边高度:
左边:5
右边:3
█████
█████
██水█
██水█
水就会从右边流出去。
所以决定水位的是:
左右最高柱子中较矮的那个
也就是:
Math.min(max_left[i], max_right[i])
最后减去当前柱子的高度:
Math.min(max_left[i], max_right[i]) - height[i]
就是这个位置实际能够存储的水量。
六、我的最终代码
class Solution {
public int trap(int[] height) {
int len = height.length;
// 记录每个位置左侧的最大高度
int[] max_left = new int[len];
max_left[0] = height[0];
// 记录每个位置右侧的最大高度
int[] max_right = new int[len];
max_right[len - 1] = height[len - 1];
// 从左向右计算左侧最大值
for(int i = 1; i < len; i++){
max_left[i] = Math.max(
height[i],
max_left[i - 1]
);
}
// 从右向左计算右侧最大值
for(int i = len - 2; i >= 0; i--){
max_right[i] = Math.max(
height[i],
max_right[i + 1]
);
}
// 计算每个位置能够接的雨水
int res = 0;
for(int i = 0; i < len; i++){
res += Math.min(
max_left[i],
max_right[i]
) - height[i];
}
return res;
}
}
七、复杂度分析
代码一共进行了三次遍历:
第一次:计算 max_left
第二次:计算 max_right
第三次:计算答案
每次都是 O(n)。
所以总时间复杂度仍然是:
O(n)
不过额外维护了两个数组:
int[] max_left
int[] max_right
所以空间复杂度为:
O(n)
八、总结
这道题我最终采用的是左右最大值数组的思路。
核心并不是模拟雨水,而是把问题拆成:
对于每个位置 i:
左边最高是多少?
右边最高是多少?
然后:
Math.min(max_left[i], max_right[i]) - height[i]
就是当前位置可以接到的雨水。
最终:
时间复杂度:O(n)
空间复杂度:O(n)
这道题让我比较明显地感受到:
很多看起来需要“模拟过程”的题目,实际上可以换一个角度,直接计算最终状态。
这里没有去模拟雨水从哪里流到哪里,而是直接计算:
每个位置最终能够存多少水。
然后把所有位置的结果加起来,就得到了最终答案。
下一步如果继续优化,就是尝试使用双指针把空间复杂度降到 O(1)。这也会是这道题更经典的一种解法。

853

被折叠的 条评论
为什么被折叠?



