力扣hot100---42.接雨水(java版)

1、题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

复制代码
 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
 输出:6
 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

复制代码
 输入:height = [4,2,0,3,2,5]
 输出:9

提示:

  • n == height.length

  • 1 <= n <= 2 * 104

  • 0 <= height[i] <= 105

2、思路

第一步:彻底理解"接雨水"的物理模型(为什么能接水?)

目标: 把现实问题抽象成数学/编程模型。

想象你面前有一排不同高度的柱子(数组 height = [0,1,0,2,1,0,1,3,2,1,2,1]),下雨了,水会从天上落下。

关键问题: 一个位置 i 上能不能积水?能积多少?

答案: 一个位置 i 能积水,当且仅当 它的左边有比它高的柱子 ,并且右边也有比它高的柱子 。水的高度由左边最高柱子右边最高柱子较矮的那个决定。

公式: 位置 i 的积水量 = Min(左边最高柱子高度, 右边最高柱子高度) - 当前柱子高度

如果这个值是负数或零,说明不能积水。

为什么是这样? 想象一个木桶,木桶能装多少水,取决于最短的那块木板。在这里,位置 i 就像木桶的底部,它的"左壁"是左边最高的柱子,"右壁"是右边最高的柱子。水位只能到"较矮的那块木板"的高度。

手动验证:height = [0,1,0,2,1,0,1,3,2,1,2,1] 为例,看位置 i=2 (高度为0):

  • 左边最高柱子:max(0,1) = 1

  • 右边最高柱子:max(2,1,0,1,3,2,1,2,1) = 3

  • 较矮的那边是 1

  • 所以积水量 = 1 - 0 = 1

完全正确!

这一步的目的是什么? 把模糊的"接水"概念,变成一个精确的、可计算的数学公式 。这是编程的第一步:量化


第二步:暴力解法------最直接的思路(先让代码跑起来)

目标: 不考虑效率,先用最笨的方法实现功能,建立信心。

既然我们知道每个位置 i 的积水量取决于它左边和右边的最大值,那最直接的想法就是:

对于数组中的每一个位置 i

  1. 向左扫描,找到 0i-1 的最大值 leftMax

  2. 向右扫描,找到 i+1n-1 的最大值 rightMax

  3. 计算 water = min(leftMax, rightMax) - height[i]

  4. 如果 water > 0,就加到总水量里。

Java 代码实现 (暴力法):

java 复制代码
public class Solution {
    public int trap(int[] height) {
        // 总水量
        int totalWater = 0;
        int n = height.length;
        // 遍历数组中的每一个位置
        for (int i = 0; i < n; i++) {
            // 1. 找左边最大值 (从0到i-1)
            int leftMax = 0;
            for (int j = 0; j < i; j++) {
                if (height[j] > leftMax) {
                    leftMax = height[j];
                }
            }
            // 2. 找右边最大值 (从i+1到n-1)
            int rightMax = 0;
            for (int j = i + 1; j < n; j++) {
                if (height[j] > rightMax) {
                    rightMax = height[j];
                }
            }
            // 3. 计算当前位置的积水量
            int waterAtI = Math.min(leftMax, rightMax) - height[i];
            // 4. 如果能积水,就加到总量里
            if (waterAtI > 0) {
                totalWater += waterAtI;
            }
        }
        return totalWater;
    }
}

分析:

  • 时间复杂度: O(n²)。因为对每个 i,我们都要遍历一次左边和右边。

  • 空间复杂度: O(1)。只用了几个变量。

为什么先写暴力法?

  1. 验证思路: 它直接对应了我们第一步的数学模型,能确保我们的核心逻辑是正确的。

  2. 建立基准: 有了一个能工作的版本,我们才能去优化它。如果优化后的代码结果不对,我们可以用暴力法的结果来对比调试。

  3. 面试加分: 在面试中,先给出一个暴力解,再优化,是标准流程,能体现你的思考过程。

这一步的目的是什么?

把数学公式翻译成最直白的代码,确保核心逻辑无误。


第三步:优化思路------动态规划(预计算,避免重复劳动)

目标: 优化暴力法中重复的扫描操作。

暴力法慢在哪里?慢在对于每个位置 i,我们都要重新扫描一遍左边和右边。这是巨大的浪费!

核心洞察: 位置 i 的左边最大值位置 i+1 的左边最大值 是有关系的!

  • leftMax[i+1] = max(leftMax[i], height[i])

同样,从右边看:

  • rightMax[i-1] = max(rightMax[i], height[i])

优化方案: 我们可以预先计算好两个数组:

  • leftMaxArray[i]: 表示从位置 0 到位置 i(包含 i)的最大高度。

  • rightMaxArray[i]: 表示从位置 i 到位置 n-1(包含 i)的最大高度。

这样,在计算每个位置 i 的积水量时,我们只需要 O(1) 的时间去查表,而不是 O(n) 的时间去扫描。

如何构建 leftMaxArray

  • leftMaxArray[0] = height[0] (第一个位置左边最大值就是它自己)

  • leftMaxArray[i] = max(leftMaxArray[i-1], height[i]) (当前位置的最大值,是前一个位置的最大值和当前高度的较大者)

如何构建 rightMaxArray

  • rightMaxArray[n-1] = height[n-1] (最后一个位置右边最大值就是它自己)

  • rightMaxArray[i] = max(rightMaxArray[i+1], height[i]) (从右往左遍历)

Java 代码实现 (动态规划):

java 复制代码
 public class Solution {
     public int trap(int[] height) {
         if (height == null || height.length == 0) {
             return 0;
         }
         int n = height.length;
         int totalWater = 0;
         // 1. 创建并填充 leftMaxArray
         int[] leftMaxArray = new int[n];
         leftMaxArray[0] = height[0];
         for (int i = 1; i < n; i++) {
             leftMaxArray[i] = Math.max(leftMaxArray[i - 1], height[i]);
         }
         // 2. 创建并填充 rightMaxArray
         int[] rightMaxArray = new int[n];
         rightMaxArray[n - 1] = height[n - 1];
         for (int i = n - 2; i >= 0; i--) {
             rightMaxArray[i] = Math.max(rightMaxArray[i + 1], height[i]);
         }
         // 3. 遍历每个位置,计算积水量
         for (int i = 0; i < n; i++) {
             int waterAtI = Math.min(leftMaxArray[i], rightMaxArray[i]) - height[i];
             if (waterAtI > 0) {
                 totalWater += waterAtI;
             }
         }
         return totalWater;
     }
 }

分析:

  • 时间复杂度: O(n)。我们遍历了3次数组(构建左数组、构建右数组、计算总水量),3n 还是 O(n)。

  • 空间复杂度: O(n)。我们额外使用了两个长度为 n 的数组。

为什么想到动态规划? 因为我发现了重叠子问题 。计算 i 的左边最大值和计算 i+1 的左边最大值,有很大一部分计算是重复的。动态规划的核心思想就是"记住已经算过的结果",避免重复计算。

这一步的目的是什么?

通过预计算和空间换时间,将时间复杂度从 O(n²) 降低到 O(n)。


第四步:终极优化------双指针(空间优化,一次遍历)

目标: 在保持 O(n) 时间复杂度的同时,将空间复杂度从 O(n) 优化到 O(1)。

动态规划法已经很快了,但它用了额外的两个数组。我们能不能不用数组,只用几个变量就搞定?

核心洞察: 我们最终要的是 min(leftMax, rightMax)。我们不需要知道精确的 左边最大值和右边最大值,我们只需要知道它们中较小的那个

双指针策略:

  • 我们用两个指针,left 从数组开头开始,right 从数组末尾开始。

  • 同时维护两个变量:leftMax (从左边遍历到目前为止遇到的最大值),rightMax (从右边遍历到目前为止遇到的最大值)。

  • 关键决策:比较 height[left]height[right]

    • 如果 height[left] < height[right]

      • 这意味着,对于 left 指针指向的位置,它的"瓶颈"一定在左边 。因为右边有一个比它高的 height[right],所以 left 位置的积水量只取决于 leftMax

      • 我们可以安全地计算 left 位置的积水量:water = leftMax - height[left] (如果 leftMax > height[left])。

      • 然后 left++

    • 如果 height[left] >= height[right]

      • 同理,对于 right 指针指向的位置,它的"瓶颈"在右边

      • 计算 right 位置的积水量:water = rightMax - height[right]

      • 然后 right--

为什么这个逻辑成立? 假设 height[left] < height[right]

  • 我们知道 leftMaxleft 左边的最大值。

  • 我们知道 height[right]left 右边的一个值,并且 height[right] > height[left]

  • 那么,left 右边所有值的最大值 rightMaxOverall 一定大于等于 height[right]

  • 所以,min(leftMax, rightMaxOverall) 的结果,一定等于 leftMax (因为 leftMax <= height[left] < height[right] <= rightMaxOverall)。

  • 因此,我们不需要知道精确的 rightMaxOverall,只需要知道 leftMax 就够了!

Java 代码实现 (双指针):

java 复制代码
public class Solution {
     public int trap(int[] height) {
         if (height == null || height.length == 0) {
             return 0;
         }
         int left = 0; // 左指针
         int right = height.length - 1; // 右指针
         int leftMax = 0; // 从左边遍历遇到的最大高度
         int rightMax = 0; // 从右边遍历遇到的最大高度
         int totalWater = 0; // 总积水量
         // 当两个指针相遇时,遍历结束
         while (left < right) {
             // 决策:哪边矮,就先处理哪边
             if (height[left] < height[right]) {
                 // 左边矮,处理左边
                 if (height[left] >= leftMax) {
                     // 更新左边见过的最大值
                     leftMax = height[left];
                 } else {
                     // 当前位置可以积水!
                     // 水量 = 左边最大值 - 当前高度
                     totalWater += leftMax - height[left];
                 }
                 left++; // 左指针右移
             } else {
                 // 右边矮 (或相等),处理右边
                 if (height[right] >= rightMax) {
                     // 更新右边见过的最大值
                     rightMax = height[right];
                 } else {
                     // 当前位置可以积水!
                     // 水量 = 右边最大值 - 当前高度
                     totalWater += rightMax - height[right];
                 }
                 right--; // 右指针左移
             }
         }
         return totalWater;
     }
     // 测试代码
     public static void main(String[] args) {
         Solution solution = new Solution();
         int[] height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
         int result = solution.trap(height);
         System.out.println("Total water trapped: " + result); // 应该输出 6
     }
 }

分析:

  • 时间复杂度: O(n)。两个指针最多遍历整个数组一次。

  • 空间复杂度: O(1)。只用了常数个额外变量。

为什么能想到双指针?

  1. 观察动态规划的数组: 我们发现 leftMaxArray 是从左到右递增的,rightMaxArray 是从右到左递增的。

  2. 寻找"瓶颈": 我们意识到决定积水量的是 min(leftMax, rightMax),而这个最小值是由"较矮"的那一侧决定的。

  3. 贪心策略: 既然较矮的一侧决定了结果,那我们就优先处理它,因为它"更确定"。

这一步的目的是什么?

在保持最优时间复杂度的同时,将空间复杂度优化到极致,达到最优解。


第五步:总结涉及的 Java 数据结构与算法知识点
  1. 数据结构:

    • 数组 (Array): 最基础的数据结构,用于存储柱子的高度。我们对它进行遍历、读取和比较。

    • 基本数据类型 (int): 用于存储高度、最大值、指针位置、总水量等。

  2. 算法思想:

    • 暴力枚举 (Brute Force): 最直接、最易理解的解法,作为基准。

    • 动态规划 (Dynamic Programming): 通过"记忆化"或"预计算"来避免重复子问题,是优化暴力法的关键。核心是找到状态转移方程 (leftMax[i] = max(leftMax[i-1], height[i]))。

    • 双指针 (Two Pointers): 一种高效的遍历技巧,特别适用于有序数组或需要从两端向中间处理的问题。在这里,它巧妙地利用了"木桶效应"和贪心思想,实现了空间优化。

    • 贪心算法 (Greedy Algorithm): 双指针法中的决策("哪边矮先处理哪边")是一种贪心策略,它在每一步都做出局部最优的选择(处理确定性更高的那一侧),最终达到全局最优。

  3. 编程技巧:

    • 边界条件处理: 代码开头检查 height == null || height.length == 0,这是良好的编程习惯。

    • 循环控制: 熟练使用 for 循环和 while 循环。

    • 条件判断: 使用 if-else 进行逻辑分支。

    • 数学函数: 使用 Math.min()Math.max()

相关推荐
D***441436 分钟前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
youngee1141 分钟前
hot100-41验证二叉搜索树
算法
迈巴赫车主43 分钟前
蓝桥杯20534爆破 java
java·数据结构·算法·职场和发展·蓝桥杯
汝生淮南吾在北1 小时前
SpringBoot+Vue在线笔记管理系统
java·vue.js·spring boot·笔记·毕业设计·毕设
坚持就完事了1 小时前
数据结构之链表
数据结构·python·算法·链表
kkkkkkkkl241 小时前
springboot日志实现
java·spring boot
Sally_xy1 小时前
安装 Docker
java·docker·容器
洛克大航海1 小时前
Maven 的下载安装配置教程
java·maven
c#上位机1 小时前
halcon图像去噪—均值滤波
图像处理·算法·均值算法·halcon