1. 接雨水问题解析:双指针法的精妙应用
这道经典的LeetCode接雨水问题,考察的是对数组特性和空间复杂度的把控能力。我第一次遇到这个问题时,直觉想到的是暴力解法,但很快意识到其O(n²)的时间复杂度在面试中肯定拿不到高分。经过反复思考和实践,最终发现双指针解法才是最优雅的解决方案。
问题的核心在于理解:对于任意一根柱子而言,它能承接的雨水量取决于它左右两侧最高柱子中的较矮者。这个看似简单的原理,在实际编码实现时却有不少值得深究的细节。下面我将从原理到实践,完整拆解这个算法的每个关键环节。
2. 双指针解法核心原理
2.1 基本思路剖析
想象一下现实中的雨水收集场景:水会从两侧向中间流动,最终的水位高度由两侧"堤坝"中较矮的一方决定。在算法中,我们正是模拟了这一自然现象。
对于数组中的每个位置i,它能承接的雨水量可以用以下公式表示:
code复制water[i] = min(left_max[i], right_max[i]) - height[i]
其中:
left_max[i]是位置i左侧的最高柱子高度right_max[i]是位置i右侧的最高柱子高度height[i]是当前位置的柱子高度
如果计算结果为负数,则表示该位置无法承接雨水,记为0。
2.2 传统解法的局限性
最直观的解法是预先计算每个位置的left_max和right_max数组:
- 从左到右遍历,记录每个位置左侧的最大高度
- 从右到左遍历,记录每个位置右侧的最大高度
- 最后遍历一次计算每个位置的积水量
这种方法虽然时间复杂度是O(n),但需要额外的O(n)空间来存储两个最大高度数组。在面试中,这通常不是最优解。
3. 双指针优化实现
3.1 算法设计思路
双指针法的精妙之处在于它通过一次遍历就完成了所有计算,且只需要常数级别的额外空间。其核心思想是:
- 使用两个指针left和right分别从数组两端向中间移动
- 维护两个变量pleft和pright,分别记录:
- pleft:left指针左侧(包括left)遇到过的最大高度
- pright:right指针右侧(包括right)遇到过的最大高度
- 每次比较pleft和pright:
- 如果pleft < pright,说明left位置的积水量由pleft决定
- 否则,right位置的积水量由pright决定
3.2 完整代码实现
cpp复制class Solution {
public:
int trap(vector<int>& height) {
int pleft = 0, pright = 0;
int left = 0;
int sum = 0;
int right = height.size() - 1;
while (left < right) {
pleft = max(pleft, height[left]);
pright = max(pright, height[right]);
if (pleft < pright) {
sum += pleft - height[left];
left++;
} else {
sum += pright - height[right];
right--;
}
}
return sum;
}
};
3.3 代码逐行解析
-
初始化阶段:
left指针从数组头部(0)开始right指针从数组尾部(height.size()-1)开始pleft和pright初始化为0,表示尚未遇到任何柱子
-
主循环逻辑:
- 更新pleft为当前left位置和之前pleft的较大值
- 更新pright为当前right位置和之前pright的较大值
- 比较pleft和pright:
- 如果pleft较小,计算left位置的积水量(pleft - height[left])并累加到sum,然后左指针右移
- 否则,计算right位置的积水量(pright - height[right])并累加到sum,然后右指针左移
-
终止条件:
- 当left >= right时,说明所有位置都已处理完毕,循环结束
- 返回累计的sum值
4. 算法正确性证明
4.1 为什么可以信赖较小的一侧?
这是双指针解法最精妙的部分。我们每次只处理较小一侧的指针,是因为:
- 当前较小的一侧的值决定了当前位置的积水量上限
- 较大的一侧可能还会遇到更高的柱子,但不会影响当前较小侧的决定
- 通过这种方式,我们确保每次计算都是基于已知的可靠信息
4.2 示例推演
以题目示例height = [0,1,0,2,1,0,1,3,2,1,2,1]为例:
初始状态:
- left=0, right=11
- pleft=0, pright=0
第一轮:
- height[0]=0, height[11]=1
- pleft=max(0,0)=0
- pright=max(0,1)=1
- 0 < 1 → 计算left位置:0-0=0,sum=0
- left右移→left=1
第二轮:
- height[1]=1
- pleft=max(0,1)=1
- pright=1
- 1 == 1 → 计算right位置:1-1=0,sum=0
- right左移→right=10
...(中间步骤省略)...
最终累计sum=6,与预期结果一致。
5. 复杂度分析与优化空间
5.1 时间复杂度
每个元素最多被访问一次(要么被left指针访问,要么被right指针访问),因此时间复杂度是严格的O(n)。
5.2 空间复杂度
只使用了固定的几个额外变量(pleft, pright, left, right, sum),因此空间复杂度是O(1),这是该算法相比动态规划解法的主要优势。
5.3 可能的优化点
虽然这个解法已经很高效,但在实际编码面试中还可以考虑:
- 使用更简洁的变量名(如用lmax代替pleft)
- 将条件判断合并到累加语句中(如原代码所示的三元运算符形式)
- 添加边界条件检查(如数组长度小于3时直接返回0)
6. 常见问题与调试技巧
6.1 为什么我的代码在某些情况下会计算错误?
常见错误原因包括:
- 没有正确处理边界条件(如空数组或单元素数组)
- 在更新pleft/pright之前就进行计算
- 移动指针的顺序错误(应该先计算再移动)
调试建议:
- 使用小规模测试用例(如[2,0,2])
- 打印每次循环后的变量状态
- 特别关注指针移动前后的数组索引
6.2 如何处理极端情况?
特殊测试用例需要考虑:
- 空数组:应返回0
- 单调递增/递减数组:应返回0
- 所有柱子等高:应返回0
- 类似"盆地"的形状(如[3,0,0,0,3])
6.3 双指针法的适用场景
双指针法特别适合解决这类"从两端向中间"处理的问题,其他适用场景包括:
- 有序数组的两数之和
- 反转字符串
- 移除元素
- 盛最多水的容器问题
在实际编码时,我发现理解双指针移动的条件是关键。对于接雨水问题,抓住"每次处理较小一侧"这个核心原则,就能确保算法的正确性。另外,在面试中如果能先解释暴力解法,再引出双指针优化,最后分析复杂度差异,往往能给面试官留下更好的印象。