1. 问题描述
先简单介绍一下题目:
给定 n 个非负整数表示每个宽度为 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 个单位的雨水(蓝色部分表示雨水)。
在梳理解题思路之前,我们可以先看着上面的示例简单思考一下。
数组的每个位置我们可以理解为放置着高度不同的柱子,当然,元素为 0 的位置就代表没放柱子。这些柱子共同构成了一个容器,雨水就在其中凹陷的地方,也就是说一个位置能存放雨水的前提就是这个位置有凹陷。
我们再来思考一下一个位置要满足什么条件,它才能有凹陷呢?其实不难想象:对于一个位置,如果它的左右两边都有比它高的柱子,那么这个位置上就会有凹陷,从而能存雨水。假如左右两边的任何一边最高的柱子低于这个位置,那么这个位置就不可能存储雨水。想明白了这一点,我们就会知道,第一个柱子(也就是数组的第一个元素)和最后一个柱子上面是绝对不可能有雨水的。
现在我们已经知道一个位置能存储雨水的条件了,接着我们想想一个位置能存储雨水的容量由什么决定。木桶效应大家应该都知道,简单来讲就是一个木桶能存储水的容量是由这个木桶最低的地方决定的。在这道题中原理也一样,一个位置能存储水的容量是和它左边最高柱子、右边最高柱子两者中较小的那一个相关的,此外,还与当前位置(桶底)的高度相关。
从而,我们就得到了下面的公式:
每个位置存储的水量 = min(左边最高柱子, 右边最高柱子) - 当前位置高度
到现在题目已经完全搞懂了,我们梳理一下解题思路。
2. 解题思路
按照上面的逻辑我们需要知道左右两边最高柱子的高度,因此需要定义两个变量分别存储两个最高高度。
我们还需要左、右两个指针,初始状态下它们分别指向最左边和最右边的柱子,哪边柱子低,我们就计算哪边柱子能存储的雨水量,然后将这一边的指针向中间靠拢一个位置,另一边的指针不动。
按照这样的逻辑,最终一定会有一个指针停在所有柱子中最高的那一个的位置,不会再动了,这个位置能存储的雨水量也不会被计算,因为它太高了,压根就存不到雨水。
这样做还有一个好处,左右两个指针每离开一个位置,就代表这个位置的雨水量已经被计算了,我们把这个雨水量加到总雨水量中,等他们两个靠拢,游戏结束。整个过程中每个元素都只被访问一次,效率也比较高。
下面我们来看代码是怎样实现的。
3. 完整代码实现
cpp
class Solution {
public:
int trap(vector<int>& height) {
int water_sum = 0;//存储水的总量
int left = 0;//左指针指向最左边
int right = height.size() - 1;//右指针指向最右边
int left_max = 0; int right_max = 0;//左右最大高度
if(height.size() <= 2) return 0;//当柱子数量不大于2时,一定存不到水
while(left < right)
{
left_max = max(height[left],left_max);//记录左指针遍历到的最大值
right_max = max(height[right],right_max);
if(height[left] < height[right])//下面讲为什么这样判断
{
water_sum += left_max - height[left];//上面讲过的逻辑
left++;
}
else
{
water_sum += right_max - height[right];
right--;
}
}
return water_sum;
}
};
- 首先要讲的点是为什么判断
height[left] < height[right]。当左指针指向的柱子高度低于右指针,那么左指针指向的柱子高度一定是低于右指针遍历过的最高的柱子的,也就是一定低于right_max,在这种情况下,能决定left位置能接多少水的就不再是右边,因为右边太高了水流不过去。再回顾我们上面的逻辑,每次循环中,永远是指向较低柱子的那个指针向中间靠拢,也就说明left左边并不存在比height[right]高的柱子,从而也不可能比right_max高了,因此left_max是两个最大值中较小的那一个。 - 还有一点就是
while循环中一定要先计算最大值,否则可能会出现某个位置存储水量为负的情况。
4. 总结
在克服重重困难并彻底理解这道题之后,我意识到双指针法最天才的地方在于:它不追求看清全局再计算,而是通过左右互搏 ,让矮的一方先走,从而确保了每一个被计算的格子,其对应的 max_left 或 max_right 确实就是限制它的那块短板。
我想这并不仅仅是一道算法题,它更像是一种人生策略:当你在低谷时,决定你上限的往往不是最远处的巅峰,而是离你最近的那块短板。我们要做的,就是不断修正自己的 left_max,在夹缝中积累能量。
本文结束。