目录
- 1.问题描述
- 2.问题分析
-
- [2.1 理解题目](#2.1 理解题目)
- [2.2 关键挑战](#2.2 关键挑战)
- [2.3 核心洞察](#2.3 核心洞察)
- [2.4 破题关键](#2.4 破题关键)
- [3 算法设计与实现](#3 算法设计与实现)
-
- [3.1 动态规划法(基础解法)](#3.1 动态规划法(基础解法))
- [3.2 双指针法](#3.2 双指针法)
- [3.3 单调栈法](#3.3 单调栈法)
- [3.4 暴力枚举法](#3.4 暴力枚举法)
- [3.5 双指针优化法](#3.5 双指针优化法)
- [4 性能分析与对比](#4 性能分析与对比)
-
- [4.1 复杂度对比](#4.1 复杂度对比)
- [4.2 性能分析](#4.2 性能分析)
- [4.3 正确性证明](#4.3 正确性证明)
- 5.边界情况处理
-
- [5.1 常见边界情况](#5.1 常见边界情况)
- [5.2 边界处理技巧](#5.2 边界处理技巧)
- 6.扩展与变体
-
- [6.1 二维接雨水问题](#6.1 二维接雨水问题)
- [6.2 最多接雨水问题](#6.2 最多接雨水问题)
- [6.3 接雨水II(有渗透问题)](#6.3 接雨水II(有渗透问题))
- [6.4 实时接雨水监控](#6.4 实时接雨水监控)
- 7.总结
-
- [7.1 核心知识点总结](#7.1 核心知识点总结)
- [7.2 算法思维提升](#7.2 算法思维提升)
- [7.3 实际应用场景](#7.3 实际应用场景)
- [7.4 面试建议](#7.4 面试建议)
1.问题描述
给定 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.length1 <= n <= 2 × 10^40 <= height[i] <= 10^5
2.问题分析
2.1 理解题目
本题要求计算柱状图中能接住的雨水量。每个柱子宽度为1,雨水只能储存在柱子之间的凹陷处。关键要点:
- 雨水计算原理:对于每个位置,能接的雨水量取决于其左右两侧最高柱子的较小值
- 木桶原理:每个位置的水量受限于左右两侧最高柱子中较矮的那个
- 边界处理:两端的柱子不能接水,因为至少需要两侧都有更高的柱子才能形成凹陷
- 水量公式:对于位置i,水量 = min(左边最高, 右边最高) - height[i](如果结果为正)
2.2 关键挑战
- 数据规模大:数组长度可达2×10^4,需要高效算法
- 多种解法:有多种解法,各有优缺点
- 边界条件:需要考虑数组长度小于3、递增递减序列等特殊情况
- 算法选择:需要根据场景选择最合适的解法
2.3 核心洞察
- 问题转化:将接雨水问题转化为求每个位置左右两侧最大值的问题
- 预处理思想:可以预先计算每个位置的左右最大值,避免重复计算
- 动态规划思路:使用两个数组分别存储每个位置左侧和右侧的最大值
- 双指针优化:可以使用双指针在一次遍历中解决问题,空间复杂度O(1)
- 单调栈应用:使用单调递减栈可以处理复杂形状的雨水收集
2.4 破题关键
- 基本思路:对于每个位置,计算其左右两侧的最高柱子,取较小值减去当前高度
- 动态规划:使用两个数组分别存储每个位置左侧和右侧的最大值
- 双指针优化:使用左右指针从两端向中间移动,动态更新左右最大值
- 单调栈:使用栈维护递减序列,遇到高柱子时计算水量
- 边界处理:位置0和n-1不能接水,因为至少需要两侧都有柱子
3 算法设计与实现
3.1 动态规划法(基础解法)
核心思想
预先计算每个位置左侧的最大值和右侧的最大值,存储在数组中,然后一次性计算所有位置的雨水量。
算法思路
- 创建左右最大值数组 :
leftMax[i]表示位置i左侧(包括自身)的最大高度rightMax[i]表示位置i右侧(包括自身)的最大高度
- 计算左侧最大值 :
- 从左到右遍历数组,
leftMax[i] = max(leftMax[i-1], height[i])
- 从左到右遍历数组,
- 计算右侧最大值 :
- 从右到左遍历数组,
rightMax[i] = max(rightMax[i+1], height[i])
- 从右到左遍历数组,
- 计算总雨水量 :
- 遍历每个位置
i,水量 =min(leftMax[i], rightMax[i]) - height[i] - 只累加正数结果
- 遍历每个位置
正确性证明
对于任意位置 i,其能接的雨水量取决于:
- 左侧最高柱子:确保左侧有边界
- 右侧最高柱子:确保右侧有边界
- 当前柱子高度:决定凹陷深度
通过预先计算左右最大值,我们可以在O(1)时间内得到每个位置的左右边界,从而正确计算雨水量。
代码实现
java
public class TrappingRainWaterDP {
public int trap(int[] height) {
if (height == null || height.length < 3) {
return 0;
}
int n = height.length;
int total = 0;
// 存储每个位置左侧的最大高度
int[] leftMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
// 存储每个位置右侧的最大高度
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
// 计算每个位置的雨水量
for (int i = 1; i < n - 1; i++) {
int water = Math.min(leftMax[i], rightMax[i]) - height[i];
if (water > 0) {
total += water;
}
}
return total;
}
}
优化版本(合并循环)
java
public class TrappingRainWaterDPOptimized {
public int trap(int[] height) {
if (height == null || height.length < 3) return 0;
int n = height.length;
int total = 0;
int[] leftMax = new int[n];
int[] rightMax = new int[n];
// 初始化左右最大值
leftMax[0] = height[0];
rightMax[n - 1] = height[n - 1];
// 同时计算左右最大值
for (int i = 1, j = n - 2; i < n; i++, j--) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
if (j >= 0) {
rightMax[j] = Math.max(rightMax[j + 1], height[j]);
}
}
// 计算总雨水量
for (int i = 1; i < n - 1; i++) {
total += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return total;
}
}
3.2 双指针法
核心思想
使用左右两个指针从两端向中间移动,实时维护左侧最大值和右侧最大值。由于水量取决于较小的最大值,所以可以动态决定计算哪一边的水量。
算法思路
- 初始化指针和最大值 :
- 左指针
left = 0,右指针right = n-1 - 左侧最大值
leftMax = height[0] - 右侧最大值
rightMax = height[n-1]
- 左指针
- 循环移动指针 :
- 当
left < right时继续循环 - 比较
leftMax和rightMax:- 如果
leftMax < rightMax:处理左指针- 左指针右移:
left++ - 更新左侧最大值:
leftMax = max(leftMax, height[left]) - 计算水量:
total += leftMax - height[left]
- 左指针右移:
- 否则:处理右指针
- 右指针左移:
right-- - 更新右侧最大值:
rightMax = max(rightMax, height[right]) - 计算水量:
total += rightMax - height[right]
- 右指针左移:
- 如果
- 当
- 返回结果:循环结束后返回总水量
正确性证明
关键问题:为什么总是移动较小最大值一侧的指针?
假设 leftMax < rightMax:
- 对于位置
left,其左侧最大值为leftMax - 右侧最大值至少为
rightMax(因为right指针在右边) - 因此,位置
left的水量受限于leftMax - 可以安全地计算
left位置的水量,然后移动左指针
同理,当 rightMax <= leftMax 时,可以安全地计算 right 位置的水量。
代码实现
java
public class TrappingRainWaterTwoPointers {
public int trap(int[] height) {
if (height == null || height.length < 3) {
return 0;
}
int left = 0;
int right = height.length - 1;
int leftMax = height[left];
int rightMax = height[right];
int total = 0;
while (left < right) {
// 总是移动较小最大值的一边
if (leftMax < rightMax) {
left++;
leftMax = Math.max(leftMax, height[left]);
total += leftMax - height[left];
} else {
right--;
rightMax = Math.max(rightMax, height[right]);
total += rightMax - height[right];
}
}
return total;
}
}
3.3 单调栈法
核心思想
使用栈存储柱子的索引,保持栈中元素对应的高度递减。当遇到一个比栈顶高的柱子时,说明形成了一个凹槽,可以接雨水。
算法思路
- 初始化栈:创建一个栈用于存储柱子的索引
- 遍历数组 :
- 当栈非空且当前柱子高度大于栈顶柱子高度时:
- 弹出栈顶元素作为凹槽底部
- 如果栈为空,跳出循环
- 计算凹槽宽度:当前索引 - 新栈顶索引 - 1
- 计算凹槽高度:min(当前高度, 新栈顶高度) - 底部高度
- 计算雨水量:宽度 × 高度
- 累加到总水量
- 将当前索引压入栈中
- 当栈非空且当前柱子高度大于栈顶柱子高度时:
- 返回结果:遍历完成后返回总水量
正确性解释
单调栈法按层计算雨水量:
- 栈中存储的是递减的柱子高度索引
- 当遇到一个更高的柱子时,栈顶元素与当前柱子及新栈顶形成一个凹槽
- 计算这个凹槽能接的雨水量
- 这种方法特别适合处理复杂形状的雨水收集
代码实现
java
import java.util.Stack;
public class TrappingRainWaterMonotonicStack {
public int trap(int[] height) {
if (height == null || height.length < 3) {
return 0;
}
Stack<Integer> stack = new Stack<>();
int total = 0;
int n = height.length;
for (int i = 0; i < n; i++) {
// 当栈非空且当前高度大于栈顶高度时
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
// 弹出栈顶元素,作为凹槽的底部
int bottom = stack.pop();
// 如果栈为空,说明左边没有柱子,无法形成凹槽
if (stack.isEmpty()) {
break;
}
// 计算凹槽宽度和高度
int left = stack.peek();
int distance = i - left - 1;
int h = Math.min(height[i], height[left]) - height[bottom];
// 累加雨水量
total += distance * h;
}
// 将当前索引入栈
stack.push(i);
}
return total;
}
}
3.4 暴力枚举法
核心思想
对于数组中的每个元素,分别向左和向右扫描,找到左侧最大值和右侧最大值,然后计算该位置能接的雨水量。
算法思路
- 遍历每个位置:从索引1到n-2(两端不能接水)
- 向左扫描:找到当前位置左侧的最大高度
- 向右扫描:找到当前位置右侧的最大高度
- 计算雨水量:min(左最大, 右最大) - 当前高度
- 累加正数结果:如果结果为正,累加到总水量
- 返回结果:遍历完成后返回总水量
时间复杂度分析
- 对于每个位置,需要向左向右扫描:O(n)
- 共有n个位置:O(n²)
- 不适用于大规模数据
代码实现
java
public class TrappingRainWaterBruteForce {
public int trap(int[] height) {
if (height == null || height.length < 3) {
return 0;
}
int n = height.length;
int total = 0;
// 遍历每个柱子(跳过第一个和最后一个)
for (int i = 1; i < n - 1; i++) {
// 找到左边最大高度
int leftMax = 0;
for (int j = i - 1; j >= 0; j--) {
leftMax = Math.max(leftMax, height[j]);
}
// 找到右边最大高度
int rightMax = 0;
for (int j = i + 1; j < n; j++) {
rightMax = Math.max(rightMax, height[j]);
}
// 计算当前柱子能接的雨水量
int water = Math.min(leftMax, rightMax) - height[i];
if (water > 0) {
total += water;
}
}
return total;
}
}
3.5 双指针优化法
核心思想
在标准双指针法的基础上,优化条件判断和移动逻辑,减少不必要的计算,提高代码执行效率。
算法思路
- 初始化指针和最大值 :
- 左指针
left = 0,右指针right = n-1 - 左侧最大值
leftMax = 0,右侧最大值rightMax = 0
- 左指针
- 循环移动指针 :
- 当
left <= right时继续循环 - 更新左侧最大值:
leftMax = max(leftMax, height[left]) - 更新右侧最大值:
rightMax = max(rightMax, height[right]) - 如果
leftMax < rightMax:- 计算左边水量:
total += leftMax - height[left] - 左指针右移:
left++
- 计算左边水量:
- 否则:
- 计算右边水量:
total += rightMax - height[right] - 右指针左移:
right--
- 计算右边水量:
- 当
- 返回结果:循环结束后返回总水量
优化点
- 提前更新最大值:在计算水量前先更新左右最大值
- 简化条件判断:直接比较左右最大值,逻辑更清晰
- 减少变量使用:使用更少的临时变量
代码实现
java
public class TrappingRainWaterTwoPointersOptimized {
public int trap(int[] height) {
if (height == null || height.length < 3) {
return 0;
}
int left = 0;
int right = height.length - 1;
int leftMax = 0;
int rightMax = 0;
int total = 0;
while (left <= right) {
// 更新左右最大值
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
// 总是先处理较小最大值的一边
if (leftMax < rightMax) {
total += leftMax - height[left];
left++;
} else {
total += rightMax - height[right];
right--;
}
}
return total;
}
}
4 性能分析与对比
4.1 复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 实现难度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 动态规划法 | O(n) | O(n) | ★★☆☆☆ | 思路清晰,易于理解 | 需要额外存储空间 |
| 双指针法 | O(n) | O(1) | ★★★☆☆ | 时间和空间都最优 | 逻辑稍复杂 |
| 单调栈法 | O(n) | O(n) | ★★★☆☆ | 可以处理复杂形状 | 空间占用大 |
| 暴力枚举法 | O(n²) | O(1) | ★☆☆☆☆ | 实现简单 | 效率低 |
| 双指针优化法 | O(n) | O(1) | ★★★☆☆ | 性能最优 | 条件判断需仔细 |
4.2 性能分析
-
时间复杂度:
- 暴力法在n=20000时不可行(约4亿次操作)
- 其他方法在n=20000时都只需要约20000次操作
-
空间复杂度:
- 双指针法最优,只需常数空间
- 动态规划和单调栈需要O(n)空间
-
实际性能:
- 在LeetCode测试中,双指针法通常最快(1-2ms)
- 动态规划稍慢(2-3ms),因为需要额外数组
- 单调栈最慢(3-5ms),因为涉及栈操作
4.3 正确性证明
双指针法的正确性可以通过以下方式证明:
假设在某一时刻,leftMax < rightMax:
- 对于位置
left,其左侧最大值为leftMax - 其右侧最大值至少为
rightMax(因为右指针在右边) - 因此,位置
left的水量由leftMax决定 - 我们可以安全地计算
left位置的水量,然后移动左指针
同理,当 rightMax <= leftMax 时,可以安全地计算 right 位置的水量。
选择建议
- 面试场景:推荐动态规划或双指针法
- 竞赛场景:双指针法,性能最优
- 生产环境:根据数据规模和内存限制选择
- 学习场景:从暴力法开始,逐步理解优化思路
5.边界情况处理
5.1 常见边界情况
- 数组长度小于3:无法形成凹槽,直接返回0
- 全零数组:所有高度为0,无法接水,返回0
- 递增或递减序列:无法形成凹槽,返回0
- 单元素或空数组:无法接水,返回0
- 中间有零:零高度的柱子也可以接水,取决于左右边界
- 相邻柱子等高:不会形成凹槽,但可能会影响后续计算
5.2 边界处理技巧
- 输入验证:方法开头检查数组长度,小于3直接返回0
- 指针边界:双指针法中,确保指针移动时不会越界
- 栈空检查:单调栈法中,弹出元素后检查栈是否为空
- 负数处理:题目限定高度为非负整数,无需处理负数
- 整数溢出 :最大水量可能达到
10^5 × 2×10^4 = 2×10^9,在int范围内,但使用long更安全
6.扩展与变体
6.1 二维接雨水问题
在二维矩阵中接雨水,每个格子有高度,雨水可以在四个方向流动。
java
public class TrappingRainWater2D {
// 这是一个更复杂的问题,通常使用优先队列(最小堆)解决
// 从边界开始,使用BFS或DFS向内部扩展
public int trapRainWater(int[][] heightMap) {
if (heightMap == null || heightMap.length < 3 || heightMap[0].length < 3) {
return 0;
}
int m = heightMap.length;
int n = heightMap[0].length;
boolean[][] visited = new boolean[m][n];
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[2] - b[2]);
// 将边界加入优先队列
for (int i = 0; i < m; i++) {
pq.offer(new int[]{i, 0, heightMap[i][0]});
pq.offer(new int[]{i, n - 1, heightMap[i][n - 1]});
visited[i][0] = true;
visited[i][n - 1] = true;
}
for (int j = 1; j < n - 1; j++) {
pq.offer(new int[]{0, j, heightMap[0][j]});
pq.offer(new int[]{m - 1, j, heightMap[m - 1][j]});
visited[0][j] = true;
visited[m - 1][j] = true;
}
int total = 0;
int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
while (!pq.isEmpty()) {
int[] cell = pq.poll();
int x = cell[0], y = cell[1], h = cell[2];
for (int[] dir : dirs) {
int nx = x + dir[0];
int ny = y + dir[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && !visited[nx][ny]) {
visited[nx][ny] = true;
total += Math.max(0, h - heightMap[nx][ny]);
pq.offer(new int[]{nx, ny, Math.max(h, heightMap[nx][ny])});
}
}
}
return total;
}
}
6.2 最多接雨水问题
在可以移除k个柱子的情况下,求最多能接多少雨水。
java
public class TrappingRainWaterWithRemoval {
/**
* 允许移除k个柱子,求最大接雨水量
* 这是一个更复杂的问题,可能需要动态规划
*/
public int trapWithRemoval(int[] height, int k) {
// 简化思路:尝试移除不同的柱子,计算最大接水量
// 实际需要更复杂的动态规划解法
int maxWater = 0;
int n = height.length;
// 暴力尝试所有可能的移除组合(仅适用于小k)
// 这里只是示意,实际需要优化
for (int mask = 0; mask < (1 << n); mask++) {
if (Integer.bitCount(mask) != k) continue;
// 创建移除后的新数组
List<Integer> newHeight = new ArrayList<>();
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) == 0) {
newHeight.add(height[i]);
}
}
// 计算接水量
int water = new TrappingRainWaterTwoPointers().trap(
newHeight.stream().mapToInt(i -> i).toArray());
maxWater = Math.max(maxWater, water);
}
return maxWater;
}
}
6.3 接雨水II(有渗透问题)
考虑雨水可以从底部渗透,或者柱子有渗透率。
java
public class TrappingRainWaterWithPermeability {
/**
* 考虑柱子有渗透率,部分水会渗透
* 渗透率数组permeability,表示每个柱子的渗透比例
*/
public int trapWithPermeability(int[] height, double[] permeability) {
if (height == null || height.length < 3) return 0;
int n = height.length;
int total = 0;
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]);
}
// 计算考虑渗透的雨水量
for (int i = 1; i < n - 1; i++) {
int water = Math.min(leftMax[i], rightMax[i]) - height[i];
if (water > 0) {
// 考虑渗透,实际接水量减少
double actualWater = water * (1 - permeability[i]);
total += (int) actualWater;
}
}
return total;
}
}
6.4 实时接雨水监控
支持动态添加新的柱子,并实时更新接雨水量。
java
public class DynamicTrappingRainWater {
private List<Integer> heights = new ArrayList<>();
private int totalWater = 0;
public void addHeight(int h) {
heights.add(h);
updateWater();
}
private void updateWater() {
// 重新计算总接水量
int[] arr = heights.stream().mapToInt(i -> i).toArray();
totalWater = new TrappingRainWaterTwoPointers().trap(arr);
}
public int getTotalWater() {
return totalWater;
}
// 优化版本:增量更新
public void addHeightOptimized(int h) {
heights.add(h);
int n = heights.size();
if (n < 3) return;
// 简化增量更新:只重新计算受影响的部分
// 实际实现更复杂,这里只是示意
updateWater(); // 实际应用中需要更精细的更新逻辑
}
}
7.总结
7.1 核心知识点总结
- 木桶原理:每个位置的水量取决于左右两边最高柱子的较小值
- 预处理思想:动态规划通过预处理数组避免重复计算
- 双指针技巧:从两端向中间扫描,实时更新左右最大值
- 单调栈应用:利用栈维护递减序列,处理凹槽问题
- 空间优化:双指针法在O(1)空间内解决问题
7.2 算法思维提升
- 问题转化能力:将接雨水问题转化为求每个位置的左右最大值问题
- 优化思维:从暴力法O(n²)到动态规划O(n),再到双指针O(1)空间的优化过程
- 数据结构应用:单调栈在处理凹槽类问题中的应用
- 边界思维:考虑各种极端情况,确保算法健壮性
7.3 实际应用场景
- 城市规划:计算城市地表雨水收集能力
- 水利工程:设计水库、水坝的容量计算
- 图像处理:处理图像中的低洼区域
- 游戏开发:地形生成中的水位计算
- 数据分析:时间序列中的低谷填充
7.4 面试建议
- 首选解法:动态规划或双指针法,根据面试官要求选择
- 解题步骤 :
- 分析问题本质:水量 = min(左最大, 右最大) - 当前高度
- 提出暴力解法:对每个位置向左右扫描
- 优化思路:预处理左右最大值数组(动态规划)
- 进一步优化:双指针法减少空间使用
- 扩展思路:单调栈处理复杂形状