📌 题目链接:leetcode.cn/problems/tr…
🔍 难度:困难 | 🏷️ 标签:数组、双指针、动态规划、单调栈
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(1)
📊题目分析
给定
n个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
💡 核心理解:
- 每个位置能“接”的水量取决于其左右两侧的最高柱子中较小的那个。
- 即:
water[i] = min(left_max[i], right_max[i]) - height[i] - 若结果小于 0,则该位置无法存水(即高度 ≥ 左右最大值之一)
🎯 示例直观说明:
height = [0,1,0,2,1,0,1,3,2,1,2,1]
对应图形如下(蓝色部分为积水):
|
| ▓▓▓▓▓▓▓▓▓▓▓▓
| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓
| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
| ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
|▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
+-----------------------------
0 1 0 2 1 0 1 3 2 1 2 1
👉 所以我们不仅要考虑当前柱子的高度,还要知道它左边和右边的最大高度。
🔍💻核心算法及代码讲解
本题最经典的解法是 双指针法(Two Pointers),属于 空间优化版的动态规划思想。
✅ 核心思想:从两边往中间收拢,利用“短板效应”
🧠 关键洞察:
对于任意位置i,能接的水由min(left_max[i], right_max[i])决定。
我们可以先预处理出每个位置的left_max和right_max,但这需要 O(n) 空间。
但我们可以用 双指针技巧 来避免额外空间开销!
🎯 为什么双指针有效?
- 假设
lmax < rmax,那么对于left指针所在的位置,它的右侧最大值一定大于等于rmax,因此min(lmax, rmax)就是lmax。 - 所以此时
left位置的蓄水量只依赖于lmax和当前高度,无需关心右侧具体最大值。 - 同理,当
rmax <= lmax时,right位置的蓄水量也仅由rmax决定。
📌 这种策略叫做 贪心 + 双指针同步推进,巧妙地避开了对整个数组的预处理。
✅ 算法步骤(伪代码)
初始化:
left = 1, right = n-2
lmax = height[0], rmax = height[n-1]
ans = 0
while left <= right:
if lmax < rmax:
ans += max(0, lmax - height[left])
lmax = max(lmax, height[left])
left++
else:
ans += max(0, rmax - height[right])
rmax = max(rmax, height[right])
right--
🧩解题思路
我们一步步来分析这个双指针过程:
- 边界处理:若数组长度 ≤ 2,无法形成凹槽,返回 0。
- 初始化两个指针:
left从左起第 1 个元素开始(index=1)right从右起第 2 个元素开始(index=n-2)
- 维护左右最大值:
lmax: 当前左侧已遍历过的最大高度rmax: 当前右侧已遍历过的最大高度
- 比较并移动指针:
- 如果
lmax < rmax,说明left侧是瓶颈,可以安全计算left处的蓄水量。 - 反之则处理
right侧。
- 如果
- 累加水量:
- 水量 =
min(left_max, right_max) - height[i] - 若为负则取 0(即不存水)
- 水量 =
📌 本质:我们在不断逼近全局最优解的过程中,局部决策不会影响最终结果,这是贪心成立的关键!
📈算法分析
| 指标 | 分析 |
|---|---|
| 时间复杂度 | O(n) —— 每个元素最多访问一次 |
| 空间复杂度 | O(1) —— 只使用常数额外空间 |
| 是否原地? | 是(无额外数组) |
| 适用场景 | 数组中求区间极值相关问题,如“接雨水”、“最大矩形”等 |
✅ 优点:
- 空间效率极高
- 思路清晰易懂
- 能应对大规模数据
⚠️ 局限性:
- 不适合需要频繁查询任意区间的最大值的情况(比如多轮询问)
- 需要对“短板原理”有深刻理解
代码 ✅
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
int trap(vector<int>& height) {
if (height.size() <= 2) {
return 0;
}
int left = 1, right = height.size() - 2;
int lmax = height[0];
int rmax = height[height.size() - 1];
int ans = 0;
while (left <= right) {
if (lmax < rmax) {
ans += height[left] >= lmax ? 0 : lmax - height[left]; // 若当前高度不低于左侧最大,则不能存水
lmax = max(lmax, height[left]); // 更新左侧最大值
left++; // 移动左指针
} else {
ans += height[right] >= rmax ? 0 : rmax - height[right]; // 若当前高度不低于右侧最大,则不能存水
rmax = max(rmax, height[right]); // 更新右侧最大值
right--; // 移动右指针
}
}
return ans;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 示例1
vector<int> height1 = {0,1,0,2,1,0,1,3,2,1,2,1};
cout << "Example 1: " << sol.trap(height1) << endl; // 输出: 6
// 示例2
vector<int> height2 = {4,2,0,3,2,5};
cout << "Example 2: " << sol.trap(height2) << endl; // 输出: 9
return 0;
}
🛠️ 补充知识:其他解法对比 🔄
虽然双指针是最优解,但了解其他方法有助于面试拓展:
✅ 方法一:暴力法(O(n²))
for each i:
left_max = max(height[0..i])
right_max = max(height[i..n-1])
water[i] = min(left_max, right_max) - height[i]
❌ 时间复杂度高,不适合大数组
✅ 方法二:动态规划预处理(O(n), O(n))
vector<int> left_max(n), right_max(n);
left_max[0] = height[0];
for(int i=1; i<n; ++i) left_max[i] = max(left_max[i-1], height[i]);
right_max[n-1] = height[n-1];
for(int i=n-2; i>=0; --i) right_max[i] = max(right_max[i+1], height[i]);
int ans = 0;
for(int i=0; i<n; ++i)
ans += min(left_max[i], right_max[i]) - height[i];
✅ 易理解,但用了 O(n) 空间
✅ 方法三:单调栈(O(n), O(n))
适用于类似“最大矩形”等问题,通过栈维护递减序列。
stack<int> stk;
int ans = 0;
for(int i=0; i<n; ++i) {
while(!stk.empty() && height[stk.top()] < height[i]) {
int top = stk.top(); stk.pop();
if(stk.empty()) break;
int width = i - stk.top() - 1;
int h = min(height[stk.top()], height[i]) - height[top];
ans += width * h;
}
stk.push(i);
}
📌 更适合解决“直方图最大矩形”类问题
💡 面试重点总结 ⭐
| 考点 | 说明 |
|---|---|
| 双指针技巧 | 必须掌握!常见于数组、字符串、链表问题 |
| 贪心思想 | “短板效应”是关键,理解为何可以跳过一侧 |
| 边界条件 | 如数组长度 ≤ 2 的情况,容易遗漏 |
| 空间优化 | 面试官常问:“如何减少空间?” → 双指针就是答案 |
| 扩展应用 | 类似题目:最大矩形、柱状图中最大的矩形、接雨水 II |
📌 经典提问:
“你有没有想过用单调栈来做这道题?”
👉 回答建议:
“是的,单调栈也可以做,但它更适合求‘以某个柱子为高的最大矩形’。而本题更自然的是双指针,因为它直接利用了‘左右最大值’的特性,并且空间最优。”
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第43题 —— 最大子数组和(简单)
🔹 题目:给定一个整数数组,找到一个具有最大和的连续子数组(至少包含一个数字),并返回其最大和。
🔹 核心思路:使用 Kadane 算法(动态规划),维护当前子数组的最大和。
🔹 考点:动态规划、状态转移、边界处理。
🔹 难度:简单,但却是 DP 入门必考题,几乎所有公司都会考察!
💡 提示:不要暴力枚举所有子数组!时间复杂度会变成 O(n²)!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!