这段代码是解决 LeetCode 42. 接雨水 问题的经典双指针解法,核心目标是计算数组所代表的 “柱子” 之间能接住的雨水总量,时间复杂度优化到 O (n),空间复杂度仅 O (1)(无需额外开辟数组)。下面从问题理解→核心思路→代码逐行解析→实例演示四个维度详细讲解:
一、问题理解
问题要求
给定一个非负整数数组 height,其中每个元素代表一根柱子的高度。下雨后,柱子之间会积水,计算这些柱子能接住的雨水总量。
例如:height = [0,1,0,2,1,0,1,3,2,1,2,1],最终能接住的雨水总量为 6(示意图中蓝色部分的面积和)。
二、核心思路:双指针 + 两侧最大值追踪
接雨水的核心逻辑是:每个位置能接住的雨水量 = 该位置左右两侧最高柱子中的较矮值 - 当前柱子的高度(若结果为正,否则为 0)。
暴力解法需要遍历每个位置并分别找左右最大值(时间 O (n²)),动态规划解法用两个数组存储左右最大值(空间 O (n))。而双指针法通过动态追踪两侧最大值,实现 “一次遍历 + O (1) 空间” 的优化,核心思路:
- 双指针初始化:左指针
left从数组开头(0)出发,右指针right从数组末尾(n-1)出发。 - 维护两侧最大值:
preMax记录左侧已遍历柱子的最大值(left左侧的最高柱),sufMax记录右侧已遍历柱子的最大值(right右侧的最高柱)。 - 按 “较矮侧” 计算接水量:
- 若
preMax < sufMax:左侧的最高柱更矮,此时left位置的接水量由preMax决定(因为右侧有更高的柱子兜底),计算后移动left指针。 - 若
preMax >= sufMax:右侧的最高柱更矮,此时right位置的接水量由sufMax决定,计算后移动right指针。
- 若
- 累加总水量:将每个位置的接水量累加到
ans中,最终得到总量。
三、代码逐行解析
java
运行
class Solution {
public int trap(int[] height) {
// 1. 初始化总雨水量(结果)
int ans = 0;
// 2. 记录左侧已遍历的最大值(preMax:left左侧的最高柱)
int preMax = 0;
// 3. 记录右侧已遍历的最大值(sufMax:right右侧的最高柱)
int sufMax = 0;
// 4. 左指针:从数组开头出发
int left = 0;
// 5. 右指针:从数组末尾出发
int right = height.length - 1;
// 6. 双指针循环:当left在right左侧时,继续计算
while (left < right) {
// 6.1 更新左侧最大值:preMax取当前preMax和height[left]的较大值
preMax = Math.max(preMax, height[left]);
// 6.2 更新右侧最大值:sufMax取当前sufMax和height[right]的较大值
sufMax = Math.max(sufMax, height[right]);
// 6.3 按较矮侧计算接水量
if (preMax < sufMax) {
// 左侧更矮:left位置的接水量 = preMax - height[left]
ans += preMax - height[left];
// 移动左指针,继续计算下一个左侧位置
left++;
} else {
// 右侧更矮(或相等):right位置的接水量 = sufMax - height[right]
ans += sufMax - height[right];
// 移动右指针,继续计算下一个右侧位置
right--;
}
}
// 7. 返回总雨水量
return ans;
}
}
四、实例演示(直观理解过程)
以经典测试用例 height = [0,1,0,2,1,0,1,3,2,1,2,1] 为例,分步演示指针移动和水量计算(数组长度 12,初始 left=0,right=11,preMax=0,sufMax=0,ans=0):
| 步骤 | left | right | height[left] | height[right] | preMax(左最大) | sufMax(右最大) | 计算逻辑(preMax vs sufMax) | 本次加水量 | ans 累计 | 指针移动 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 11 | 0 | 1 | max(0,0)=0 | max(0,1)=1 | 0 < 1 → 左侧计算:0-0=0 | +0 | 0 | left→1 |
| 2 | 1 | 11 | 1 | 1 | max(0,1)=1 | sufMax=1 | 1 = 1 → 右侧计算:1-1=0 | +0 | 0 | right→10 |
| 3 | 1 | 10 | 1 | 2 | preMax=1 | max(1,2)=2 | 1 < 2 → 左侧计算:1-1=0 | +0 | 0 | left→2 |
| 4 | 2 | 10 | 0 | 2 | max(1,0)=1 | sufMax=2 | 1 < 2 → 左侧计算:1-0=1 | +1 | 1 | left→3 |
| 5 | 3 | 10 | 2 | 2 | max(1,2)=2 | sufMax=2 | 2 = 2 → 右侧计算:2-2=0 | +0 | 1 | right→9 |
| 6 | 3 | 9 | 2 | 1 | preMax=2 | max(2,1)=2 | 2 = 2 → 右侧计算:2-1=1 | +1 | 2 | right→8 |
| 7 | 3 | 8 | 2 | 2 | preMax=2 | max(2,2)=2 | 2 = 2 → 右侧计算:2-2=0 | +0 | 2 | right→7 |
| 8 | 3 | 7 | 2 | 3 | preMax=2 | max(2,3)=3 | 2 < 3 → 左侧计算:2-2=0 | +0 | 2 | left→4 |
| 9 | 4 | 7 | 1 | 3 | max(2,1)=2 | sufMax=3 | 2 < 3 → 左侧计算:2-1=1 | +1 | 3 | left→5 |
| 10 | 5 | 7 | 0 | 3 | max(2,0)=2 | sufMax=3 | 2 < 3 → 左侧计算:2-0=2 | +2 | 5 | left→6 |
| 11 | 6 | 7 | 1 | 3 | max(2,1)=2 | sufMax=3 | 2 < 3 → 左侧计算:2-1=1 | +1 | 6 | left→7 |
| 结束 | 7 | 7 | 3 | 3 | - | - | left 不小于 right,循环结束 | - | 6 | - |
最终结果:ans=6,与预期一致。
五、关键细节:为什么 “移动较矮侧” 能正确计算?
核心逻辑是:接水量由两侧最高柱中的较矮者决定,而双指针移动规则确保了 “较矮侧的最大值是确定的”:
-
当
preMax < sufMax时:左侧的最高柱(preMax)比右侧的最高柱(sufMax)矮,此时left位置的右侧必然存在至少sufMax高的柱子(因为sufMax是右侧已遍历的最大值),因此left位置的接水量仅由preMax决定(无需关心右侧更远的柱子,因为sufMax已经足够高)。计算后移动left指针,继续处理下一个左侧位置。 -
当
preMax >= sufMax时:右侧的最高柱(sufMax)更矮,此时right位置的左侧必然存在至少preMax高的柱子,因此right位置的接水量由sufMax决定。计算后移动right指针,继续处理下一个右侧位置。
这种方式避免了重复计算两侧最大值,仅用一次遍历就完成了所有位置的接水量累加。
六、复杂度分析
- 时间复杂度:O (n)。左右指针从两端向中间移动,每个元素最多被访问一次,总操作次数与数组长度成正比。
- 空间复杂度:O (1)。仅使用了
ans、preMax、sufMax、left、right5 个变量,无额外空间开销(相比动态规划解法的 O (n) 空间,优势明显)。
七、总结
该解法的核心是 **“双指针 + 动态维护两侧最大值”**:通过比较两侧最大值的大小,确定当前位置的接水量由哪一侧决定,进而移动指针缩小范围。这种思路用极简的空间实现了线性时间复杂度,是接雨水问题的最优解法之一,也体现了 “贪心策略” 在区间问题中的高效应用(通过局部最优选择逐步逼近全局解)。
625

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



