一、题目背景介绍
题目原文
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
-
示例输入:
height = [0,1,0,2,1,0,1,3,2,1,2,1] -
示例输出:
6
核心解题原理铺垫
单个位置能储存的雨水计算公式: water\[i\] = \\min(左侧最高柱子高度,\\ 右侧最高柱子高度) - 当前柱子高 要求结果不能小于 0(柱子本身很高无法存水时存水量取 0),把数组所有下标位置的存水量累加,就是总接雨体积。 拿示例下标 2(数值 0)举例:左边最高是 1、右边最高是 2,取最小值 1,1-0=,这个格子可以存 1 单位雨水,逐个点位求和最终总和为 6。
二、方案 1:暴力双重循环(直观易懂,适合新手理解逻辑)
算法思路
遍历数组每一个下标i:
-
内层向左扫描,找出
[0,i-1]区间的柱子最大值leftMax -
内层向右扫描,找出
[i+1,len-1]区间的柱子最大值rightMax -
按公式计算点位存水量,负数直接归 0,累加全局结果
Java 完整代码
public class TrapRainBrute {
public int trap(int[] height) {
int totalWater = 0;
int len = height.length;
// 下标首尾位置左右无围挡,一定无法存水,直接跳过
for(int i = 1; i < len - 1; i++){
int leftMax = 0;
// 遍历左侧找最高柱
for(int l = 0; l < i; l++){
leftMax = Math.max(leftMax, height[l]);
}
int rightMax = 0;
// 遍历右侧找最高柱
for(int r = i + 1; r < len; r++){
rightMax = Math.max(rightMax, height[r]);
}
int cur = Math.min(leftMax, rightMax) - height[i];
totalWater += Math.max(cur, 0);
}
return totalWater;
}
public static void main(String[] args) {
TrapRainBrute solution = new TrapRainBrute();
int[] input = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(solution.trap(input)); // 控制台打印结果:6
}
}
复杂度分析
-
时间复杂度:O(n\^2,外层循环遍历 n 次,每个点位左右各做一次线性扫描,数据量偏大时会超时
-
空间复杂度:O(1,仅使用若干临时变量,无额外数组开销
三、方案 2:动态规划预处理(优化时间,工业场景常用)
算法思路
暴力解法重复做大量区间最大值扫描,动态规划提前做预处理:
-
新建
leftMaxDp数组:leftMaxDp[i]记录下标i左侧(含自身)柱子的最大高度,从左向右遍历填充 -
新建
rightMaxDp数组:rightMaxDp[i]记录下标i右侧(含自身)柱子的最大高度,从右向左遍历填充 -
遍历所有下标,套用存水公式累加总和
Java 完整代码
public class TrapRainDp {
public int trap(int[] height) {
int len = height.length;
if(len <= 2) return 0;
int[] leftMaxDp = new int[len];
int[] rightMaxDp = new int[len];
leftMaxDp[0] = height[0];
// 从左预处理左侧最大值
for(int i = 1; i < len; i++){
leftMaxDp[i] = Math.max(leftMaxDp[i - 1], height[i]);
}
rightMaxDp[len - 1] = height[len - 1];
// 从右预处理右侧最大值
for(int i = len - 2; i >= 0; i--){
rightMaxDp[i] = Math.max(rightMaxDp[i + 1], height[i]);
}
int totalWater = 0;
for(int i = 0; i < len; i++){
totalWater += Math.min(leftMaxDp[i], rightMaxDp[i]) - height[i];
}
return totalWater;
}
public static void main(String[] args) {
TrapRainDp solution = new TrapRainDp();
int[] input = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(solution.trap(input)); // 输出6
}
}
复杂度分析
-
时间复杂度:O(n,三轮线性遍历数组,不存在嵌套循环
-
空间复杂度:O(n,需要两个长度和入参一致的 DP 数组做预处理存储
四、方案 3:双指针(最优空间,面试高频标准答案)
算法原理
用左右指针left(起始位置 0)、right(末尾位置len-1)双向逼近,同步维护两侧动态最大值leftMax、rightMax:
-
若
leftMax < rightMax:左侧围挡上限更小,left位置的储水量由左边挡板约束,计算left点位雨水,左指针右移更新左侧最大值 -
反之右侧挡板约束储水量,处理
right点位雨水,右指针左移更新右侧最大值 全程不依赖额外 DP 数组,原地完成计算
Java 完整代码
public class TrapRainTwoPointer {
public int trap(int[] height) {
int len = height.length;
if(len <= 2) return 0;
int left = 0, right = len - 1;
int leftMax = 0, rightMax = 0;
int totalWater = 0;
while(left < right){
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if(leftMax < rightMax){
totalWater += leftMax - height[left];
left++;
}else{
totalWater += rightMax - height[right];
right--;
}
}
return totalWater;
}
public static void main(String[] args) {
TrapRainTwoPointer solution = new TrapRainTwoPointer();
int[] input = {0,1,0,2,1,0,1,3,2,1,2,1};
System.out.println(solution.trap(input)); // 输出6
}
}
复杂度分析
-
时间复杂度:O(n,左右指针只会相向移动,数组仅遍历 1 次
-
空间复杂度:O(1,只定义基础整型变量,无数组、容器类额外内存占用
五、三种方案横向对比 & 面试答题建议
|---------|------------|---------|---------------------|
| 实现方案 | 时间复杂度 | 空间复杂度 | 适用场景 |
| 暴力双重循环 | O(n\^2 | O(1 | 算法入门理解原理、小规模数据调试 |
| 动态规划预处理 | O(n | O(n | 工程开发、代码可读性优先的业务场景 |
| 双向双指针 | O(n | O(1 | 算法笔试、技术面试(追求最优空间性能) |
踩坑避坑总结
-
数组长度小于 3 时(柱子不足 3 根无法围成凹槽)直接返回 0,规避数组下标越界、负数储水量问题
-
储水量公式结果出现负数(当前柱子高于两侧挡板),必须归零,不能做负数累加
-
双指针不要写反指针移动逻辑:哪边挡板上限更小,就先结算哪一侧点位的雨水
六、拓展延伸(单调栈补充思路)
除上面三类写法,该题还能借助单调递减栈做解法:遍历柱子,栈内存放柱子下标,遇到更高柱子时弹出栈顶作为凹槽底部,用新柱子与栈内新栈顶作为左右围挡结算雨水,时间复杂度O(n、空间O(n,适合延伸练习栈结构的应用场景。
七、结语
接雨水是数组区间最值类算法的标杆题目,从暴力循环到 DP 预处理再优化成双指针,完整体现了「先理解朴素逻辑、再做冗余扫描优化、最后压缩内存开销」通用算法优化思路,在字节、阿里等大厂的 Java 后端算法笔试中出现频次很高,建议熟练掌握双指针写法,能兼顾时间、空间两项性能指标。