在LeetCode的hard难度题目中,「42. 接雨水」是经典的数组应用题,核心考察对"边界约束"的理解和空间复杂度优化能力。本文将从题目本质出发,先剖析双指针解法的核心逻辑并完成优化,再补充前缀/后缀数组法、单调栈法两种主流方案,全面覆盖不同复杂度需求下的实现思路,兼顾原理讲解与代码实操。
一、题目回顾:接雨水的核心逻辑
题目描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算下雨之后能接多少雨水。
核心原理:每个柱子能接住的雨水量,取决于它左右两侧的"最高柱子高度的较小值"与自身高度的差值------即 water[i] = min(leftMax[i], rightMax[i]) - height[i](若结果为正则累加,否则为0)。
常见解法分为三类,各有优劣,适配不同场景:
-
暴力法(O(n²)时间,O(1)空间):遍历每个柱子,分别找左右最大值,效率过低,仅作思路参考。
-
前缀/后缀数组法(O(n)时间,O(n)空间):提前存储左右最大值数组,逻辑直观,空间开销较大。
-
双指针法(O(n)时间,O(1)空间):最优性能解法,动态维护左右最大值,无需额外数组存储。
-
单调栈法(O(n)时间,O(n)空间):基于栈维护递减序列,适合理解"层叠积水"逻辑,空间开销中等。
下文先聚焦双指针解法的优化,再逐一讲解其他两种实用方法。
二、双指针解法:从可用到优雅
2.1 原代码实现与剖析
typescript
function trap(height: number[]): number {
const hL = height.length;
let left = 0;
let leftMax = height[0];
let right = hL - 1;
let rightMax = height[hL - 1];
let res = 0;
while (left < right) {
if (height[left] <= height[right]) {
left++;
if (height[left] > leftMax) {
leftMax = height[left];
} else {
res += leftMax - height[left];
}
} else {
right--;
if (height[right] > rightMax) {
rightMax = height[right];
} else {
res += rightMax - height[right];
}
}
}
return res;
};
原代码核心逻辑正确,已达 O(n) 时间、O(1) 空间的最优性能,但存在边界处理缺失、变量命名模糊、逻辑顺序冗余等问题,需针对性优化。
2.2 优化后代码
typescript
function trap(height: number[]): number {
// 边界处理:数组长度小于3无法接水,直接返回0
if (height.length < 3) return 0;
let left = 0;
let right = height.length - 1;
let leftMax = 0; // 左指针左侧的最大值
let rightMax = 0; // 右指针右侧的最大值
let water = 0; // 总接水量
while (left < right) {
// 先更新当前侧的最大值(保证最大值的有效性)
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
// 矮侧决定接水量,处理矮侧并移动指针
if (leftMax <= rightMax) {
water += leftMax - height[left];
left++;
} else {
water += rightMax - height[right];
right--;
}
}
return water;
}
2.3 优化要点说明
-
鲁棒性提升:补充数组长度小于3的边界判断,规避空数组、短数组导致的越界错误。
-
可读性优化 :变量
hL改为通用的height.length,res改为语义化的water,逻辑更清晰。 -
简洁性提升 :调整"更新最大值→计算水量→移动指针"的顺序,贴合核心公式,用
Math.max替代冗余if-else。
优化后代码性能不变,且适配所有合法输入场景,更符合工程实践需求。
三、其他解法:多思路拓展
3.1 前缀/后缀数组法(直观易理解)
该方法直接对应接雨水的核心原理,通过两个辅助数组提前存储每个位置的左右最大值,再遍历数组计算总水量,逻辑直观,适合新手理解。
实现思路
-
构建前缀最大值数组
leftMaxArr:leftMaxArr[i]表示第 i 个柱子左侧(含自身)的最高高度。 -
构建后缀最大值数组
rightMaxArr:rightMaxArr[i]表示第 i 个柱子右侧(含自身)的最高高度。 -
遍历每个柱子,累加
min(leftMaxArr[i], rightMaxArr[i]) - height[i](结果为正才累加)。
代码实现
typescript
function trap(height: number[]): number {
const n = height.length;
if (n < 3) return 0;
// 前缀最大值数组
const leftMaxArr = new Array(n).fill(0);
leftMaxArr[0] = height[0];
for (let i = 1; i < n; i++) {
leftMaxArr[i] = Math.max(leftMaxArr[i - 1], height[i]);
}
// 后缀最大值数组
const rightMaxArr = new Array(n).fill(0);
rightMaxArr[n - 1] = height[n - 1];
for (let i = n - 2; i >= 0; i--) {
rightMaxArr[i] = Math.max(rightMaxArr[i + 1], height[i]);
}
// 计算总接水量
let water = 0;
for (let i = 0; i < n; i++) {
water += Math.min(leftMaxArr[i], rightMaxArr[i]) - height[i];
}
return water;
}
复杂度分析
时间复杂度 O(n):需三次遍历数组(前缀、后缀、计算水量),总次数为 3n,渐进复杂度为 O(n)。
空间复杂度 O(n):需两个长度为 n 的辅助数组,空间开销与数组长度线性相关。
3.2 单调栈法(层叠积水视角)
该方法从"层叠积水"的角度思考,用单调栈维护一个递减的柱子高度序列,当遇到比栈顶高的柱子时,说明形成了积水区域,通过栈顶元素计算该区域的积水量。
实现思路
-
栈中存储柱子的索引,保证栈内索引对应的高度严格递减。
-
遍历数组,若当前柱子高度小于等于栈顶索引对应的高度,直接入栈(维持递减序列)。
-
若当前柱子高度大于栈顶索引对应的高度,弹出栈顶元素(作为积水区域的底部),计算该区域的宽度和高度,累加积水量。
-
重复步骤2-3,直至遍历完所有柱子。
代码实现
typescript
function trap(height: number[]): number {
const n = height.length;
if (n < 3) return 0;
const stack: number[] = []; // 存储柱子索引,维持高度递减序列
let water = 0;
for (let i = 0; i < n; i++) {
// 当栈不为空且当前高度大于栈顶高度,说明形成积水区域
while (stack.length > 0 && height[i] > height[stack[stack.length - 1]]) {
const bottomIdx = stack.pop()!; // 积水区域的底部索引
// 栈空则无左边界,无法形成积水,退出循环
if (stack.length === 0) break;
const leftIdx = stack[stack.length - 1]; // 积水区域的左边界索引
// 积水高度 = 左右边界的较小值 - 底部高度
const waterHeight = Math.min(height[leftIdx], height[i]) - height[bottomIdx];
// 积水宽度 = 当前索引 - 左边界索引 - 1
const waterWidth = i - leftIdx - 1;
// 累加积水量
water += waterHeight * waterWidth;
}
stack.push(i);
}
return water;
}
复杂度分析
时间复杂度 O(n):每个柱子最多入栈、出栈各一次,总操作次数为 2n,渐进复杂度为 O(n)。
空间复杂度 O(n):最坏情况下(柱子高度严格递减),栈内会存储所有柱子索引,空间开销为 O(n)。
四、解法对比与场景选择
为方便大家根据需求选择合适解法,整理对比表格如下:
| 解法 | 时间复杂度 | 空间复杂度 | 核心优势 | 适用场景 |
|---|---|---|---|---|
| 双指针法 | O(n) | O(1) | 性能最优,无额外空间开销 | 空间敏感场景、面试最优解 |
| 前缀/后缀数组法 | O(n) | O(n) | 逻辑直观,易理解和实现 | 新手学习、代码快速迭代场景 |
| 单调栈法 | O(n) | O(n) | 从层叠视角理解积水,拓展性强 | 复杂积水场景分析、算法思路拓展 |
五、总结:算法优化与思路拓展
「接雨水」问题的核心是抓住"边界最大值约束水量"的本质,三种解法虽思路不同,但都围绕这一核心展开:
-
双指针法通过动态维护左右最大值,实现了性能最优,是面试中的首选解法,需重点掌握其"矮侧优先处理"的逻辑。
-
前缀/后缀数组法是核心原理的直接落地,适合新手入门,帮助理解接雨水的本质逻辑。
-
单调栈法提供了全新的视角,通过栈维护递减序列,能精准定位每个积水区域,拓展了数组问题的解题思路。
在实际应用中,需根据空间限制、代码可读性需求选择合适解法;面试中,优先掌握双指针法,同时能讲解其他解法的思路,更能体现对问题的深度理解。