LeetCode 42.接雨水

LeetCode 42: 接雨水

题目描述:

给定一个包含非负整数的数组 height,每个元素表示一个柱子的高度。每个柱子宽度为 1,计算按此排列的柱子,下雨之后能接多少雨水。


题目分析

接雨水的关键在于:

  1. 每一个位置能接多少雨水?
    一个柱子位置 i 处能接的雨水量由左右两边的最大高度决定,可以计算为:

    复制代码
    水量(i) = max(0, min(左侧最大高度, 右侧最大高度) - height[i])。
    • 如果左侧或右侧的最大高度小于当前柱子高度,那么当前位置无法存水。
    • 关键是如何高效地计算每个位置左右两侧的最大高度。

解法 1:暴力解法

思路:

  1. 遍历数组,对于位置 i,分别计算其左侧和右侧的最大高度:

    • leftMax:从 0i 的最大值。
    • rightMax:从 in-1 的最大值。
  2. 根据公式计算雨水量:

    java 复制代码
    rain[i] = Math.max(0, Math.min(leftMax, rightMax) - height[i]);

模板代码:

java 复制代码
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int totalWater = 0;

        for (int i = 0; i < n; i++) {
            int leftMax = 0, rightMax = 0;

            // 找到左边的最大高度
            for (int j = 0; j <= i; j++) {
                leftMax = Math.max(leftMax, height[j]);
            }

            // 找到右边的最大高度
            for (int j = i; j < n; j++) {
                rightMax = Math.max(rightMax, height[j]);
            }

            // 计算当前位置能接的雨水
            totalWater += Math.max(0, Math.min(leftMax, rightMax) - height[i]);
        }

        return totalWater;
    }
}

时间和空间复杂度:

  • 时间复杂度:O(N²)
    两层嵌套循环分别计算左右最大高度。
  • 空间复杂度:O(1)
    没有额外使用数组。

解法 2:预处理 + 动态规划

思路:

  1. 使用两个数组 leftMaxrightMax 分别存储当前位置左侧和右侧的最大高度,使用 动态规划 来提前计算:

    • leftMax[i]:表示从 0i 的最大高度:

      复制代码
      leftMax[i] = Math.max(leftMax[i-1], height[i])。
    • rightMax[i]:表示从 in-1 的最大高度:

      复制代码
      rightMax[i] = Math.max(rightMax[i+1], height[i])。
  2. 遍历一次数组,按公式计算雨水量。

模板代码:

java 复制代码
class Solution {
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0) return 0;

        // 预处理左侧和右侧的最大高度
        int[] leftMax = new int[n];
        int[] rightMax = new int[n];

        // 计算左侧最大
        leftMax[0] = height[0];
        for (int i = 1; i < n; i++) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        // 计算右侧最大
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        // 计算雨水总量
        int totalWater = 0;
        for (int i = 0; i < n; i++) {
            totalWater += Math.max(0, Math.min(leftMax[i], rightMax[i]) - height[i]);
        }

        return totalWater;
    }
}

时间和空间复杂度:

  • 时间复杂度:O(N)
    预处理中遍历两次数组,计算leftMaxrightMax,然后最后遍历一次数组。
  • 空间复杂度:O(N)
    需要额外的 leftMaxrightMax 数组。

解法 3:双指针

思路:

  • 使用两个指针 leftright 分别指向数组的两端。
  • 维护两个变量 leftMaxrightMax
    • leftMax:从左向右扫描时左侧的最大高度。
    • rightMax:从右向左扫描时右侧的最大高度。
  • 每次比较 leftMaxrightMax
    • 如果 leftMax < rightMax,则左指针位置的水量取决于 leftMax
    • 否则右指针位置的水量取决于 rightMax

模板代码:

java 复制代码
class Solution {
    public int trap(int[] height) {
        int left = 0, right = height.length - 1;
        int leftMax = 0, rightMax = 0;
        int totalWater = 0;

        while (left <= right) {
            if (leftMax < rightMax) {
                if (height[left] < leftMax) {
                    totalWater += leftMax - height[left];
                } else {
                    leftMax = height[left];
                }
                left++;
            } else {
                if (height[right] < rightMax) {
                    totalWater += rightMax - height[right];
                } else {
                    rightMax = height[right];
                }
                right--;
            }
        }

        return totalWater;
    }
}

时间和空间复杂度:

  • 时间复杂度:O(N)
    双指针只需要遍历一次数组。
  • 空间复杂度:O(1)
    没有使用额外的存储空间。

解法 4:单调栈

思路:

  • 使用栈来存储柱子的索引。
  • 在遍历数组时,维护一个递减栈:
    • 如果当前柱子高度小于栈顶柱子,直接入栈。
    • 如果当前柱子高度大于栈顶柱子,则「尝试闭合洼地」:
      • 栈顶弹出,视为洼地底部。

      • 如果栈不为空,计算当前闭合的水量:

        复制代码
        高度 = min(左边高度, 右边高度) - 洼地底部高度。
        宽度 = 当前索引 - 栈顶索引 - 1。
      • 累积水量。

模板代码:

java 复制代码
class Solution {
    public int trap(int[] height) {
        Stack<Integer> stack = new Stack<>();
        int totalWater = 0;

        for (int i = 0; i < height.length; i++) {
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int bottom = stack.pop(); // 弹出洼地底部
                
                if (stack.isEmpty()) break; // 如果栈为空,无法形成洼地

                int left = stack.peek(); // 左边柱子的索引
                int width = i - left - 1; // 宽度
                int waterHeight = Math.min(height[i], height[left]) - height[bottom]; // 高度
                totalWater += width * waterHeight;
            }
            stack.push(i);
        }

        return totalWater;
    }
}

时间和空间复杂度:

  • 时间复杂度:O(N)
    每个柱子仅入栈/出栈一次。
  • 空间复杂度:O(N)
    栈的空间取决于柱子的数量。

经典变体题目

  1. 11. 盛最多水的容器

    • 问题:寻找两个柱子之间的最大盛水面积。
    • 解法:双指针法(面积 = min(height[left], height[right]) × 宽度)。
  2. 407. 接雨水 II(3D 版本)

    • 问题:给定一个二维矩阵,表示高度图,计算可以接的最大雨水。
    • 解法:使用最小堆 + BFS 模拟水位。
  3. 接雨水的连续子区间

    • 问题:问「特定区间」的接水量。
    • 解法:动态规划或双指针进一步改造。
  4. 接雨水路径优化

    • 限制接雨水的路径是否能达到最低点,同时积水量递推。

如何快速 AC?

  • 优先掌握 双指针法(时间 O(N),空间 O(1)),适合面试快速实现。
  • 在需要更复杂分析时,掌握 动态规划单调栈
  • 对变体题目,坚持递归 + 贪心 + 栈的方法融合应对。
相关推荐
Tiger_shl12 分钟前
【Python语言基础】24、并发编程
java·数据库·python
05091521 分钟前
测试基础笔记第十一天
java·数据库·笔记
A charmer39 分钟前
【MySQL】数据库基础
数据库·mysql
pjx9871 小时前
应用的“体检”与“换装”:精通Spring Boot配置管理与Actuator监控
数据库·spring boot·oracle
松树戈1 小时前
PostgreSQL 分区表——范围分区SQL实践
数据库·sql·postgresql
bing_1581 小时前
Redis 的单线程模型对微服务意味着什么?需要注意哪些潜在瓶颈?
数据库·redis·微服务
小黑蛋学java1 小时前
Redis-cli常用参数及功能的详细说明
redis
wangzhongyudie1 小时前
SQL实战:01之行转列实现
数据库·sql
斗鹰一余洛晟1 小时前
工作中sql总结
数据库·sql
阿七想学习2 小时前
MySQL《事务》
数据库·mysql