解题思路
这道题是经典的接雨水 问题。核心是计算每个柱子位置能积多少水:对于位置 i,它能积的水量 = min(左边最高柱子, 右边最高柱子) - height[i](如果为正)。
方法一:动态规划(预处理左右最大值)
- 用两个数组
leftMax[i]和rightMax[i]分别记录位置i左边和右边的最高高度。 - 然后遍历一次计算每个位置的积水量。
- 时间 O(n),空间 O(n)。
方法二:双指针(空间 O(1))
- 利用双指针从两端向中间收缩,同时维护
leftMax和rightMax。 - 每次移动较矮一侧的指针,因为该侧的积水量只由该侧的
leftMax或rightMax与当前高度决定(较矮一侧的 max 已经确定是短板)。 - 时间 O(n),空间 O(1)。
这里采用更优的双指针法。
Java 代码实现(双指针)
java
class Solution {
public int trap(int[] height) {
// 边界情况:长度小于3时无法积水
if (height == null || height.length < 3) {
return 0;
}
int left = 0; // 左指针
int right = height.length - 1; // 右指针
int leftMax = 0; // 当前遍历过的左侧最高柱子
int rightMax = 0; // 当前遍历过的右侧最高柱子
int totalWater = 0; // 总积水量
// 当左右指针未相遇时,持续计算
while (left < right) {
// 哪侧矮就处理哪侧,因为积水量由矮侧决定
if (height[left] < height[right]) {
// 左侧柱子较矮
if (height[left] >= leftMax) {
// 当前柱子比左侧最高还要高,更新 leftMax,此时该柱子上方无法积水
leftMax = height[left];
} else {
// 当前柱子低于左侧最高,说明可以积水
totalWater += leftMax - height[left];
}
left++; // 左指针右移
} else {
// 右侧柱子较矮(或相等)
if (height[right] >= rightMax) {
// 当前柱子比右侧最高还要高,更新 rightMax
rightMax = height[right];
} else {
// 可以积水
totalWater += rightMax - height[right];
}
right--; // 右指针左移
}
}
return totalWater;
}
}
关键点注释与解释
- 双指针的核心思想 :积水的多少由两侧的"短板"决定。我们维护
leftMax(从左边看到当前位置的最高柱子)和rightMax(从右边看到当前位置的最高柱子)。当height[left] < height[right]时,说明leftMax一定小于rightMax(因为rightMax至少等于height[right],而height[right] > height[left])。此时left位置的积水量完全由leftMax决定,与右侧无关,因此可以先计算left处的水量,然后left++。同理,右侧较低时,先处理右指针。 - **为什么这样不会漏算?**我们每次都处理当前较矮的一侧,并且只依赖该侧的历史最大值。由于另一侧的柱子更高,保证了该侧的积水不会从另一端流走,因此计算的积水量是准确的。
- 相等时移动哪边?
height[left] == height[right]时,移动哪边都可以,代码中归到了else分支(移动right),不影响最终结果。 - 复杂度分析:时间 O(n),每个元素最多访问一次;空间 O(1),只用了几个变量。
示例演示
以 height = [0,1,0,2,1,0,1,3,2,1,2,1] 为例:
| 步骤 | left | right | heightleft | heightright | leftMax | rightMax | 操作 | 总水量 |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 11 | 0 | 1 | 0 | 0 | 左矮,0<0? 更新leftMax=0,left++ | 0 |
| 2 | 1 | 11 | 1 | 1 | 0 | 0 | 左矮,1>=0? 更新leftMax=1,left++ | 0 |
| 3 | 2 | 11 | 0 | 1 | 1 | 0 | 左矮,0<1? 积水+1,left++ | 1 |
| 4 | 3 | 11 | 2 | 1 | 1 | 0 | 右矮,处理右:1>=0? 更新rightMax=1,right-- | 1 |
| 5 | 3 | 10 | 2 | 2 | 1 | 1 | 右矮,2>=1? 更新rightMax=2,right-- | 1 |
| 6 | 3 | 9 | 2 | 1 | 1 | 2 | 右矮,1<2? 积水+1,right-- | 2 |
| 7 | 3 | 8 | 2 | 2 | 1 | 2 | 右矮,2>=2? 更新rightMax=2,right-- | 2 |
| 8 | 3 | 7 | 2 | 3 | 1 | 2 | 左矮,2>=1? 更新leftMax=2,left++ | 2 |
| 9 | 4 | 7 | 1 | 3 | 2 | 2 | 左矮,1<2? 积水+1,left++ | 3 |
| 10 | 5 | 7 | 0 | 3 | 2 | 2 | 左矮,0<2? 积水+2,left++ | 5 |
| 11 | 6 | 7 | 1 | 3 | 2 | 2 | 左矮,1<2? 积水+1,left++ | 6 |
| 12 | 7 | 7 | 相遇 | - | - | - | 结束 | 6 |
最终总水量为 6,与预期一致。
其他解法(动态规划)参考
如果追求思路简单,也可以用 O(n) 空间的方法:
java
public int trap(int[] height) {
if (height == null || height.length < 3) return 0;
int n = height.length;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
int water = 0;
for (int i = 0; i < n; i++) {
int minBound = Math.min(leftMax[i], rightMax[i]);
if (minBound > height[i]) {
water += minBound - height[i];
}
}
return water;
}
面试中常见法。