接雨水(LeetCode 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
解题思路
💡 思路一:暴力法(不推荐,仅用于理解)
对每个柱子,找出其左边最大高度和右边最大高度,当前柱子能接的雨水为:
water[i] = max(0, min(leftMax, rightMax) - height[i])
- 时间复杂度高,O(n²),空间 O(1)
✅ 思路二:动态规划(预处理左右最大值)
- 创建两个数组
leftMax和rightMax:leftMax[i]表示从0到i的最大高度rightMax[i]表示从i到n-1的最大高度
- 再遍历一次,累加每个位置的雨水量
优点 :清晰易懂
缺点:需要额外 O(n) 空间
✅ 思路三:双指针(最优解)
利用双指针从两端向中间移动:
- 维护
leftMax和rightMax - 若
leftMax < rightMax,则左侧柱子的雨水量由leftMax决定(因为右侧有更高的墙) - 反之亦然
优点:时间 O(n),空间 O(1),效率最高
✅ 思路四:单调栈
使用单调递减栈存储索引:
- 当遇到比栈顶高的柱子时,说明可以形成凹槽
- 弹出栈顶,计算以该位置为底的雨水面积
适用于"找最近更大元素"类问题,但本题中不如双指针简洁
代码实现
java:Java
// 方法一:动态规划
class Solution {
public int trap(int[] height) {
if (height == null || height.length == 0) 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++) {
water += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return water;
}
}
// 方法二:双指针(推荐)
class Solution {
public int trap(int[] height) {
if (height == null || height.length == 0) return 0;
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0;
int water = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
water += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
water += rightMax - height[right];
}
right--;
}
}
return water;
}
}
go:Go
// 方法一:动态规划
func trap(height []int) int {
n := len(height)
if n == 0 {
return 0
}
leftMax := make([]int, n)
rightMax := make([]int, n)
leftMax[0] = height[0]
for i := 1; i < n; i++ {
leftMax[i] = max(leftMax[i-1], height[i])
}
rightMax[n-1] = height[n-1]
for i := n - 2; i >= 0; i-- {
rightMax[i] = max(rightMax[i+1], height[i])
}
water := 0
for i := 0; i < n; i++ {
water += min(leftMax[i], rightMax[i]) - height[i]
}
return water
}
// 方法二:双指针(推荐)
func trap(height []int) int {
if len(height) == 0 {
return 0
}
left, right := 0, len(height)-1
leftMax, rightMax := 0, 0
water := 0
for left < right {
if height[left] < height[right] {
if height[left] >= leftMax {
leftMax = height[left]
} else {
water += leftMax - height[left]
}
left++
} else {
if height[right] >= rightMax {
rightMax = height[right]
} else {
water += rightMax - height[right]
}
right--
}
}
return water
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
示例演示
以 height = [0,1,0,2,1,0,1,3,2,1,2,1] 为例:
| i | height[i] | leftMax | rightMax | min(L,R) | water[i] |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 3 | 0 | 0 |
| 1 | 1 | 1 | 3 | 1 | 0 |
| 2 | 0 | 1 | 3 | 1 | 1 |
| 3 | 2 | 2 | 3 | 2 | 0 |
| 4 | 1 | 2 | 3 | 2 | 1 |
| 5 | 0 | 2 | 3 | 2 | 2 |
| 6 | 1 | 2 | 3 | 2 | 1 |
| 7 | 3 | 3 | 3 | 3 | 0 |
| 8 | 2 | 3 | 2 | 2 | 0 |
| 9 | 1 | 3 | 2 | 2 | 1 |
| 10 | 2 | 3 | 2 | 2 | 0 |
| 11 | 1 | 3 | 1 | 1 | 0 |
✅ 总雨水 = 1+1+2+1+1 = 6
答案有效性证明
关键观察 :
一个位置 i 能接的雨水高度,取决于其左侧最高墙 和右侧最高墙中的较小者。
引理 :若
leftMax[i] ≤ rightMax[i],则i处的积水高度为leftMax[i] - height[i](前提是leftMax[i] > height[i])
双指针正确性:
- 当
height[left] < height[right]时,leftMax ≤ rightMax必然成立(因为rightMax至少为height[right]) - 因此
left位置的积水由leftMax决定,无需知道全局rightMax - 同理适用于右侧
通过数学归纳法可严格证明算法正确性。
复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力法 | O(n²) | O(1) | 每个位置遍历左右 |
| 动态规划 | O(n) | O(n) | 两次预处理 + 一次计算 |
| 双指针 | O(n) | O(1) | 最优解,推荐使用 |
| 单调栈 | O(n) | O(n) | 每个元素入栈出栈一次 |
问题总结
📌 核心思想:每个位置的积水由"短板效应"决定------即左右两侧最高墙的较小值。
✅ 最佳实践:双指针法在保证线性时间的同时,将空间压缩到常数级别,是面试和竞赛中的首选解法。
💡 延伸思考:
- 二维接雨水(LeetCode 407)可用优先队列(最小堆)解决
- 若允许修改原数组,可进一步优化空间(但通常不建议)
掌握本题的多种解法,有助于深入理解前缀/后缀最值 、双指针技巧 和单调栈的应用场景。
github地址: https://github.com/swf2020/LeetCode-Hot100-Solutions