
✨ 把代码写进星轨,用逻辑丈量宇宙。
| 导航 | 链接 |
|---|---|
| 个人主页 | 🏠 星轨初途 |
| 基础语言专栏 | 💻 C语言、📚 数据结构 |
| 刷题实战专栏 | 🚀 算法及编程题分享、📝 力扣每日刷题分享 |
🌧️ 又到了每日刷题时间!今天这道《接雨水》算是非常经典的一道题了
(ง •_•)ง一道题可以用双向扫描、前后缀、双指针、单调栈四种方式解决,正好一次把几个常见思想串起来!
文章目录
接雨水

题目思路
对于某一个位置 i 来说,它能够接多少水,取决于它左右两边最高的柱子。
假设:
text
左边最高柱子 = leftMax
右边最高柱子 = rightMax
那么当前位置能够达到的最高水位就是:
text
min(leftMax, rightMax)
因此当前位置能够接到的水为:
cpp
min(leftMax, rightMax) - height[i]
所以这道题的核心其实就是:
找到每个位置左右两边能够形成的边界,再计算这个位置可以接多少水。
下面用四种方法来解决。
方法一:左右两次扫描
这是我最开始写的方法。
先从左向右扫描,使用 preh 记录当前左边界高度。
如果当前柱子比 preh 矮:
cpp
curnum += preh - h;
先把这部分水暂时记录下来。
如果后面找到了:
cpp
h >= preh
说明右边出现了一堵足够高的墙,这一段水确定可以接住:
cpp
cnt += curnum;
然后更新新的左边界。
但是如果一直到数组末尾都没有找到比左边界更高的柱子,那么最后暂存的水不能直接计算,因为右边可能挡不住。
所以记录:
cpp
prei
也就是左侧最后确定的边界位置,然后再从右往左扫描一次,把剩余区域计算出来。
复杂度分析
- 时间复杂度:
O(n); - 空间复杂度:
O(1)。
代码
cpp
class Solution
{
public:
int trap(vector<int>& height)
{
int n = height.size();
int cnt = 0;
int curnum = 0;
int preh = height[0];
int prei = 0;
// 从左向右计算
for (int i = 1; i < n; i++)
{
int h = height[i];
if (h >= preh)
{
// 找到足够高的右边界,这部分水可以确定
cnt += curnum;
curnum = 0;
preh = h;
prei = i;
}
else
{
// 暂存当前能够接的水
curnum += preh - h;
}
}
// 从右向左处理剩余部分
preh = height[n - 1];
curnum = 0;
for (int i = n - 2; i >= prei; i--)
{
int h = height[i];
if (h >= preh)
{
cnt += curnum;
curnum = 0;
preh = h;
}
else
{
curnum += preh - h;
}
}
return cnt;
}
};
这个方法的核心就是:
text
左边能确定的 → 从左向右算
右边剩下的 → 从右向左算
不需要额外数组,空间复杂度只有 O(1)。
方法二:前后缀最大值
这个方法是最好理解的一种。
对于每个位置 i,只要知道:
text
左边最高柱子
右边最高柱子
就能直接算出当前位置接多少水。
先用 pre[i] 保存:
text
0 ~ i 范围内的最高柱子
再用 end[i] 保存:
text
i ~ n-1 范围内的最高柱子
于是:
cpp
min(pre[i], end[i])
就是当前位置的水位。
当前位置的水量:
cpp
min(pre[i], end[i]) - height[i]
复杂度分析
- 时间复杂度:
O(n); - 空间复杂度:
O(n)。
代码
cpp
class Solution
{
public:
int trap(vector<int>& height)
{
int n = height.size();
// pre[i]:i 左边(包括自己)的最高柱子
vector<int> pre(n);
pre[0] = height[0];
for (int i = 1; i < n; i++)
{
pre[i] = max(pre[i - 1], height[i]);
}
// end[i]:i 右边(包括自己)的最高柱子
vector<int> end(n);
end[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--)
{
end[i] = max(end[i + 1], height[i]);
}
int cnt = 0;
// 每个位置能够接的水
for (int i = 0; i < n; i++)
{
cnt += min(pre[i], end[i]) - height[i];
}
return cnt;
}
};
这种方法非常直观,但是需要两个额外数组。
方法三:双指针优化
既然前后缀方法真正需要的只是:
text
左侧最高值
右侧最高值
那么其实没有必要把所有位置的最大值全部保存下来。
可以使用两个变量:
cpp
pre
end
分别记录当前左右两边的最高柱子。
再使用:
cpp
left
right
从数组两端向中间移动。
如果:
cpp
pre <= end
说明左边当前的短板是 pre。
右侧已经存在一个高度至少为 pre 的柱子,所以 left 位置能接多少水已经可以直接确定:
cpp
pre - height[left]
然后:
cpp
left++;
反过来,如果:
cpp
pre > end
那么就可以确定右边的水量:
cpp
end - height[right]
然后:
cpp
right--;
复杂度分析
- 时间复杂度:
O(n); - 空间复杂度:
O(1)。
代码
cpp
class Solution
{
public:
int trap(vector<int>& height)
{
int cnt = 0;
// 左右两边目前遇到的最高柱子
int pre = 0, end = 0;
int left = 0, right = height.size() - 1;
while (left < right)
{
pre = max(pre, height[left]);
end = max(end, height[right]);
// 左边比较低,左边水量已经可以确定
if (pre <= end)
{
cnt += pre - height[left];
left++;
}
else
{
// 右边比较低,右边水量已经可以确定
cnt += end - height[right];
right--;
}
}
return cnt;
}
};
这个方法可以看成:
把前后缀数组压缩成了两个变量。
所以既保持了 O(n) 的时间复杂度,又把空间复杂度优化到了 O(1)。
方法四:单调栈
前面三种方法基本都是从竖着计算每根柱子上方有多少水的角度考虑。
单调栈则换了一个方向:
横着一层一层计算水。
使用一个栈保存柱子的下标,并保持栈中柱子高度整体单调递减。
当遇到一个更高的柱子 height[i] 时:
cpp
height[st.top()] <= height[i]
说明当前柱子可以和栈中的左边柱子形成一个凹槽。
先取出凹槽底部:
cpp
int pre_h = height[st.top()];
弹出之后,新的栈顶就是左边界:
cpp
int left = st.top();
当前 i 就是右边界。
这一层水的高度:
cpp
min(height[left], height[i]) - pre_h
宽度:
cpp
i - left - 1
所以这一层水量:
cpp
dh * (i - left - 1)
然后继续弹栈,相当于一层一层向上计算。
复杂度分析
- 时间复杂度:
O(n); - 空间复杂度:
O(n)。
虽然存在 while,但是每个元素最多:
text
入栈一次
出栈一次
所以整体依然是 O(n)。
代码
cpp
class Solution
{
public:
int trap(vector<int>& height)
{
int ans = 0;
// 存储柱子的下标
stack<int> st;
for (int i = 0; i < height.size(); i++)
{
int h = height[i];
// 当前柱子比栈顶高,可以形成凹槽
while (!st.empty() && height[st.top()] <= h)
{
// 凹槽底部高度
int pre_h = height[st.top()];
st.pop();
// 没有左边界,无法接水
if (st.empty())
{
break;
}
// 左边界
int left = st.top();
// 当前这一层水的高度
int dh = min(height[left], height[i]) - pre_h;
// 高 × 宽
ans += dh * (i - left - 1);
}
st.push(i);
}
return ans;
}
};
例如出现:
text
3 _ 2
| |
| 1 |
单调栈会把中间较矮的柱子作为"坑底",找到左右边界以后计算这一层能够接多少水。
如果凹槽有多层,就不断弹栈:
text
第一层 → 算一次
第二层 → 再算一次
第三层 → 再算一次
最终把所有层的面积累加起来。
四种方法总结
| 方法 | 核心思路 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 左右两次扫描 | 左边确定一部分,再从右边补剩余部分 | O(n) |
O(1) |
| 前后缀最大值 | 保存每个位置左右最高柱子 | O(n) |
O(n) |
| 双指针 | 用两个变量代替前后缀数组 | O(n) |
O(1) |
| 单调栈 | 横向计算每个凹槽中的水 | O(n) |
O(n) |
这四种方法其实可以分成两类:
cpp
按列计算:
左右扫描
前后缀
双指针
按层计算:
单调栈
其中前后缀方法最容易理解,而双指针则是在它的基础上进一步进行了空间优化。
单调栈的思维方式和前面几种完全不同,它不是考虑:
text
"这一根柱子上面有多少水?"
而是考虑:
text
"这个凹槽这一层能装多少水?"
所以第一次接触的时候会稍微绕一点,但理解以后还是非常有意思的
( •̀ ω •́ )✧
🎉 今天的《接雨水》就拿下啦!一道题连着练到了前后缀、双指针和单调栈,含金量还是相当高的。
有时候一道困难题不一定要只记住一种写法,多换几个角度去看,反而更容易把算法之间的联系串起来
٩(ˊᗜˋ*)و每日一题继续打卡,我们下一题再见!🌟