接雨水问题解析:双指针与单调栈解法
接雨水问题解析:双指针与单调栈解法
问题描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子下雨之后能接多少雨水。这个问题可以形象地理解为:在由一系列柱子组成的"地形"上方下雨,雨水会积存在柱子之间的凹陷处,我们需要计算这些凹陷处总共能存储多少雨水。
示例
示例 1:
高度图:[0,1,0,2,1,0,1,3,2,1,2,1]
图形表示:■■ ■ ■■ ■ ■■■ ■ ■ ■ ■
0 1 0 2 1 0 1 3 2 1 2 1
输出:6
解释:可以接 6 个单位的雨水(■表示柱子,~表示雨水):
■
■~~~~■ ■
■ ■~■■■ ■ ■ ■ ■
示例 2:
高度图:[4,2,0,3,2,5]
图形表示:
■
■ ■
■ ■ ■ ■
■ ■ ■ ■
■ ■ ■ ■
4 2 0 3 2 5
输出:9
解释:可以接 9 个单位的雨水:
■
■~~~~~■
■ ■~■ ■
■ ■ ■ ■
■ ■ ■ ■
解法一:双指针法(最优解)
思路分析
双指针法是一种高效且空间复杂度低的解决方案。其核心思想是:
- 使用左右两个指针分别从数组两端向中间移动,类似于快速排序的分区操作
- 维护左右两边的最大值(lmax和rmax),这两个值决定了当前可以接多少雨水
- 根据左右最大值中的较小值来决定计算哪一侧的雨水,因为水位高度由较低的一侧决定
算法步骤详解
- 初始化左右指针
l = 0
和r = height.size() - 1
- 初始化左右最大值
lmax = 0
和rmax = 0
- 当
l < r
时循环:- 更新左右最大值:
lmax = max(lmax, height[l])
(记录从左到目前位置的最大高度)rmax = max(rmax, height[r])
(记录从右到目前位置的最大高度)
- 比较当前左右指针处的高度:
- 如果
height[l] < height[r]
:- 计算左指针处的雨水量:
lmax - height[l]
(因为右边有更高的柱子) - 将结果累加到总雨水数中
- 左指针右移(
l++
)
- 计算左指针处的雨水量:
- 否则:
- 计算右指针处的雨水量:
rmax - height[r]
(因为左边有更高的柱子) - 将结果累加到总雨水数中
- 右指针左移(
r--
)
- 计算右指针处的雨水量:
- 如果
- 更新左右最大值:
- 返回累计的雨水量
代码实现
class Solution {
public:int trap(vector<int>& height) {if(height.empty()) return 0; // 边界条件处理int l = 0, r = height.size() - 1;int ans = 0;int lmax = height[l]; // 初始化为第一个元素int rmax = height[r]; // 初始化为最后一个元素while(l < r) {// 更新左右最大值lmax = max(lmax, height[l]);rmax = max(rmax, height[r]);// 决定移动哪一侧指针if(height[l] < height[r]) {ans += lmax - height[l]; // 计算当前列的雨水量++l;} else {ans += rmax - height[r]; // 计算当前列的雨水量--r;}}return ans;}
};
复杂度分析
- 时间复杂度:O(n),只需遍历一次数组
- 空间复杂度:O(1),只使用了常数个额外变量(l, r, ans, lmax, rmax)
实际应用场景
这种解法非常适合处理大规模数据,比如:
- 城市建筑群的雨水收集系统设计
- 地形分析中的积水区域计算
- 游戏开发中的物理引擎水位模拟
解法二:单调栈法
思路分析
单调栈法通过维护一个单调递减的栈来计算雨水:
- 遍历高度数组
- 维护一个单调递减栈(存储索引),栈底到栈顶高度递减
- 当当前高度大于栈顶高度时,说明可能形成"凹槽"可以接住雨水
- 计算凹槽的宽度和高度,进而计算雨水量并累加
详细步骤
- 初始化一个空栈和结果变量ans=0
- 遍历高度数组(i从0到n-1):
- 当栈不为空且当前高度height[i] > 栈顶高度height[st.top()]:
- 弹出栈顶元素作为凹槽底部(top = st.top())
- 如果栈为空,说明没有左边界,无法形成凹槽,跳出循环
- 计算凹槽宽度:当前索引i - 新栈顶索引l - 1
- 计算凹槽高度:min(height[l], height[i]) - height[top]
- 将宽度×高度加到ans中
- 将当前索引i压入栈中
- 当栈不为空且当前高度height[i] > 栈顶高度height[st.top()]:
- 返回累计的雨水量ans
代码实现
class Solution {
public:int trap(vector<int>& height) {int ans = 0;stack<int> st; // 存储柱子的索引int n = height.size();for(int i = 0; i < n; i++) {// 当当前柱子高于栈顶柱子时,可能存在凹槽while(!st.empty() && height[i] > height[st.top()]) {int top = st.top(); // 凹槽底部位置st.pop(); // 弹出栈顶if(st.empty()) break; // 没有左边界int l = st.top(); // 左边界位置int curwidth = i - l - 1; // 凹槽宽度int curheight = min(height[l], height[i]) - height[top]; // 凹槽高度ans += curwidth * curheight; // 计算当前凹槽的雨水量}st.push(i); // 将当前柱子压入栈中}return ans;}
};
复杂度分析
- 时间复杂度:O(n),每个元素最多被压入和弹出栈一次
- 空间复杂度:O(n),最坏情况下(如单调递减的数组)栈的大小为n
适用场景
单调栈法特别适合:
- 需要直观理解雨水形成过程的场景
- 需要分步计算每个凹槽雨水量的情况
- 教学和算法理解阶段
方法对比与总结
方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
双指针法 | O(n) | O(1) | 空间效率高,代码简洁 | 理解起来稍抽象 |
单调栈法 | O(n) | O(n) | 直观,易于理解 | 空间消耗较大 |
选择建议
- 面试场景:优先使用双指针法,展示对最优解的理解
- 教学场景:可以先讲解单调栈法,再引入双指针优化
- 实际应用:根据内存限制选择,内存紧张时用双指针法
扩展思考
- 如何修改算法计算二维平面的接雨水问题?
- 如果柱子不是直立的,而是有倾斜角度,算法该如何调整?
- 如何实时计算动态变化的地形中的积水量?
理解这两种经典解法不仅有助于解决接雨水问题,也为处理其他类似的区间计算问题(如最大矩形面积、柱状图中最大矩形等)提供了思路框架。