【LeetCode 刷题系列 | 第 1 篇】前端老司机拆解接雨水问题,从暴力到最优解💦
2026年1月19日 09:56
🌧️ 前言
Hello~大家好。我是秋天的一阵风
欢迎来到我的 LeetCode 刷题系列专栏~ 作为一名深耕前端多年的老司机,我深知算法能力对前端工程师的重要性 —— 它不仅能帮我们在面试中脱颖而出,更能提升日常业务代码的逻辑严谨性和性能优化能力。
今天咱们要攻克的是 LeetCode 中的经典 hard 题「接雨水」,这道题堪称 “面试高频钉子户”,考察的核心是对数组遍历和边界判断的理解。
很多同学一开始会被它唬住,但只要咱们从基础思路慢慢拆解,再逐步优化,就能轻松拿捏!话不多说,咱们直奔主题~
一、LeetCode 接雨水题目详情
1. 题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
2. 示例演示
- 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
- 输出:6
- 解释:如题目中的高度图所示,下雨后能接住 6 单位的雨水。
- 输入:height = [4,2,0,3,2,5]
- 输出:9
3. 难度级别
🔴 困难:这道题之所以被归为困难,是因为它需要突破常规的遍历思维,从 “局部最优” 推导 “全局最优”。但只要掌握了核心逻辑,它其实是一道 “纸老虎” 题~
二、解题思路大剖析
1. 暴力解法:直捣黄龙的基础思路
暴力解法的核心逻辑很朴素:逐个计算每个柱子能接的雨水量,最后求和。而一个柱子能接多少水,完全取决于它左右两侧的 “最高屏障”—— 也就是左右两侧最高柱子中较矮的那一个,用这个较矮值减去当前柱子的高度,就是该位置能接的水量(若结果为负,则接 0 水)。
核心步骤拆解:
- 遍历数组中的每一个柱子(索引从 0 到 n-1);
- 对当前柱子 i,向左遍历所有柱子,找到左侧的最高高度
leftMax;
- 对当前柱子 i,向右遍历所有柱子,找到右侧的最高高度
rightMax;
- 计算当前柱子的接水量:
Math.max(0, Math.min(leftMax, rightMax) - height[i]);
- 累加所有柱子的接水量,得到总水量。
分步拆解演示(以输入 [0,1,0,2,1,0,1,3,2,1,2,1] 为例):
咱们逐个扒开每个位置的接水逻辑,从索引 0 开始:
- 索引 0:左侧没有柱子,leftMax=0;右侧最高柱子高度是 3。min(0,3)=0,接水量 0-0=0;
- 索引 1:左侧最高是 0,右侧最高是 3。min(0,3)=0,接水量 0-1=-1,取 0;
- 索引 2:左侧遍历 [0,1],最高是 1;右侧遍历 [2,1,0,1,3,2,1,2,1],最高是 3。min(1,3)=1,接水量 1-0=1;
- 索引 3:左侧最高是 1,右侧最高是 3。min(1,3)=1,接水量 1-2=-1,取 0;
- 索引 4:左侧最高是 2,右侧最高是 3。min(2,3)=2,接水量 2-1=1;
- 索引 5:左侧最高是 2,右侧最高是 3。min(2,3)=2,接水量 2-0=2;
- 索引 6:左侧最高是 2,右侧最高是 3。min(2,3)=2,接水量 2-1=1;
- 索引 7:左侧最高是 2,右侧最高是 2。min(2,2)=2,接水量 2-2=0;
- 索引 8:左侧最高是 3,右侧最高是 2。min(3,2)=2,接水量 2-2=0;
- 索引 9:左侧最高是 3,右侧最高是 2。min(3,2)=2,接水量 2-1=1;
- 索引 10:左侧最高是 3,右侧最高是 1。min(3,1)=1,接水量 1-2=-1,取 0;
- 索引 11:右侧没有柱子,接水量 0;
把这些有效接水量相加:1+1+2+1+1=6,和示例输出一致!
JavaScript 代码实现(暴力解法):
/**
* @param {number[]} height
* @return {number}
*/
var trap = function(height) {
const n = height.length;
let total = 0; // 总接水量
// 遍历每个柱子(除了首尾也可以,但不影响结果,代码更简洁)
for (let i = 0; i < n; i++) {
let leftMax = 0; // 左侧最高柱子高度
let rightMax = 0; // 右侧最高柱子高度
// 向左遍历,找左侧最高
for (let j = i; j >= 0; j--) {
leftMax = Math.max(leftMax, height[j]);
}
// 向右遍历,找右侧最高
for (let j = i; j < n; j++) {
rightMax = Math.max(rightMax, height[j]);
}
// 计算当前柱子的接水量,累加到总水量
total += Math.min(leftMax, rightMax) - height[i];
}
return total;
};
// 测试用例
console.log(trap([0,1,0,2,1,0,1,3,2,1,2,1])); // 输出6
console.log(trap([4,2,0,3,2,5])); // 输出9
暴力解法的优缺点:
- 优点:思路简单直观,容易理解,适合作为入门思路;
- 缺点:时间复杂度极高,为 O (n²)(每个柱子都要左右遍历一次),当 n 较大时(比如 10^4)会直接超时,不适合实际面试场景。
2. 双指针解法:空间优化的最优思路
暴力解法的问题在于 “重复遍历”,双指针的核心是利用左右两侧的最大值关系,在一次遍历中完成计算,把时间复杂度降到 O (n),空间复杂度优化到 O (1)。
核心原理:
- 定义左右指针 left(初始 0)和 right(初始 n-1);
- 定义 leftMax(左侧已遍历的最高高度)和 rightMax(右侧已遍历的最高高度);
- 当 height[left] <= height[right] 时,左侧的最大值 leftMax 决定了当前 left 位置的接水量(因为右侧有更高的柱子兜底);
- 反之,右侧的最大值 rightMax 决定当前 right 位置的接水量;
- 遍历过程中不断更新 leftMax、rightMax 和总水量,直到指针相遇。
JavaScript 代码实现(双指针解法):
/**
* @param {number[]} height
* @return {number}
*/
var trap = function(height) {
const n = height.length;
if (n < 3) return 0; // 少于3个柱子无法接水
let left = 0, right = n - 1;
let leftMax = 0, rightMax = 0;
let total = 0;
while (left < right) {
// 左侧柱子更矮,以leftMax为基准
if (height[left] <= height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left]; // 更新左侧最高
} else {
total += leftMax - height[left]; // 计算当前位置接水量
}
left++; // 左指针右移
} else {
// 右侧柱子更矮,以rightMax为基准
if (height[right] >= rightMax) {
rightMax = height[right]; // 更新右侧最高
} else {
total += rightMax - height[right]; // 计算当前位置接水量
}
right--; // 右指针左移
}
}
return total;
};
// 测试用例
console.log(trap([0,1,0,2,1,0,1,3,2,1,2,1])); // 输出6
console.log(trap([4,2,0,3,2,5])); // 输出9
三、复杂度分析
1. 时间复杂度
- 暴力解法:O (n²),双层循环导致重复遍历;
- 双指针解法:O (n),一次遍历完成所有计算;
2. 空间复杂度
- 暴力解法:O (1),仅使用常数级额外空间;
- 双指针解法:O (1),同样仅使用常数级额外空间;
总结
好啦,今天的接雨水问题就讲到这里!相信大家已经对这道题的各种解法了如指掌。如果你有更优的思路,或者在刷题过程中遇到了疑问,欢迎在评论区留言讨论~
下一篇专栏,咱们将攻克另一道前端面试高频题,猜猜是什么?关注我,刷题不迷路!咱们下期再见~ 👋