【LeetCode Hot100 刷题日记(7/100)】42. 接雨水 —— 双指针优化法、数组、动态规划、单调栈🌧️📊

15 阅读6分钟

📌 题目链接: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_maxright_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--

🧩解题思路

我们一步步来分析这个双指针过程:

  1. 边界处理:若数组长度 ≤ 2,无法形成凹槽,返回 0。
  2. 初始化两个指针
    • left 从左起第 1 个元素开始(index=1)
    • right 从右起第 2 个元素开始(index=n-2)
  3. 维护左右最大值
    • lmax: 当前左侧已遍历过的最大高度
    • rmax: 当前右侧已遍历过的最大高度
  4. 比较并移动指针
    • 如果 lmax < rmax,说明 left 侧是瓶颈,可以安全计算 left 处的蓄水量。
    • 反之则处理 right 侧。
  5. 累加水量
    • 水量 = 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²)!


📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!