当前位置: 首页 > news >正文

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

1、题目描述

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

示例 1:

img

输入: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 代码实现 (暴力法):

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 代码实现 (动态规划):

 public class Solution {public int trap(int[] height) {if (height == null || height.length == 0) {return 0;}int n = height.length;int totalWater = 0;// 1. 创建并填充 leftMaxArrayint[] 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. 创建并填充 rightMaxArrayint[] 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 代码实现 (双指针):

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()

http://www.dtcms.com/a/565503.html

相关文章:

  • 长春公司建站模板三把火科技网站设计
  • Nine.fun:连接现实娱乐与Web3经济的全新生态
  • 【职业方向】2026小目标,从web开发转型web3开发【一】
  • 用 Playwright + 容器化做分布式浏览器栈:调度、会话管理与资源回收
  • 148.PCIE参考时钟无法绑定
  • 国际网站如何做seo电脑网站模版
  • LeetCode 414 - 第三大的数
  • HAProxy 配置实操 (OpenEuler为例)
  • 前端(Vue框架)实现主题切换
  • 国外代理网站wordpress需要多少内存
  • 投资手机网站源码如何利用源代码做网站
  • Redisson在Spring Boot中的高并发应用解析
  • NOFX AI量化交易系统 - 完整使用手册
  • 别人把我做的网站_我自己现在想把网站背景改掉_我要怎么改wordpress 翻译不起作用
  • 网站建设要咨询哪些店铺推广是如何收费的
  • 智能建站网业车怎么打车
  • 玩转Rust高级应用 如何进行面向对象设计模式的实现,实现状态模式
  • B2B中药饮片电商平台是什么?其主要特征和价值是什么?
  • 无锡公司网站制作深圳5区发布通知
  • lamp做网站的论文微平台网站开发
  • 【Linux网络编程】初识网络,理解TCP/IP五层模型
  • 如何分析linux相关的系统日志
  • 网页设计作业--接口文档的撰写
  • 第一次找人做网站微信运营专员是什么工作
  • vue2中的.native修饰符和$listeners组件属性
  • 网站建设情况报告范文wordpress首页怎么控制
  • 家政小程序拓展分析:从工具型产品到全链路服务生态的技术落地与商业破局
  • 中国十大门户网站排行定远建设小学投诉网站
  • 外贸网站制作哪家快wordpress删除站点
  • 检查部门网站建设网站建设的主题