跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

LeetCode 42 接雨水:动态规划、单调栈与双指针解法(Java)

LeetCode 42 接雨水问题的三种解法。动态规划通过预存左右最大高度计算,时间 O(n) 空间 O(n)。双指针利用左右最大值较小的一侧移动,优化至空间 O(1)。单调栈维护递减栈,横向计算凹槽面积。三者各有优劣,代码均提供 Java 实现。

古灵精怪发布于 2026/3/27更新于 2026/10/23101 浏览
LeetCode 42 接雨水:动态规划、单调栈与双指针解法(Java)

一、引言

LeetCode 42 接雨水是数组类算法题的经典代表,也是面试中的高频考点。这道题的核心是理解'每个位置接雨水量由左右最大高度决定',而动态规划、单调栈、双指针则是解决该问题的三大核心思路。

二、动态规划

2.1 解题步骤

朴素的做法是对于数组 height 中的每个元素,分别向左和向右扫描并记录左边和右边的最大高度,然后计算每个下标位置能接的雨水量。

我们可以清晰地看出可以接水的格子是左边最高和右边最高都高于自己本身的高度,也就是凹下去,才能成功接水。而接水的高度则是左边右边最高中较矮的一侧减去自己本身。

所以我们的解题步骤是:

  • 先计算出当前柱子左边最高的柱子和右边最高的柱子
  • *当前柱子接水面积 = (min(左高,右高) - 自身高度)1
  • 将面积累加。

2.2 动态规划的使用

但是我们每次计算柱子的左高和右高都去遍历一次数组其实时间复杂度会达到 O(n^2),所以我们可以使用动态规划。

动态规划(Dynamic Programming,简称 DP)核心是把复杂问题拆解成多个可重复解决的子问题,并把子问题的答案保存下来(避免重复计算),最终通过子问题的解推导出原问题的解。

  • 我们把计算接水总面积拆分成计算单个柱子的接水面积
  • 并且记录下单个柱子的左高和右高,方便计算下一个柱子的左高和右高
  • 最后再把单个柱子的面积累加

2.3 代码实现

/**
 * 动态规划
 * @param height 柱子的高度
 * @return 接住雨水的总数
 */
public int trap(int[] height) {
    int len = height.length;
    // 当柱子个数小于三时,无法接到雨水
    // 接雨水的条件是两边的比中间高,所以至少要有三个柱子
    if (len < 3) {
        return 0;
    }
    //记录第 i 根柱子左边(包括自身)中最高柱子的高度
    int[] leftMax = new int[len];
    leftMax[0] = height[0];
    for (int i = 1; i < len; ++i) {
        //复用上一根柱子的左高,优化了时间复杂度
        leftMax[i] = Math.max(leftMax[i - 1], height[i]);
    }
    
    [] rightMax =  [len];
    rightMax[len - ] = height[len - ];
     (   len - ; i >= ; --i) {
        rightMax[i] = Math.max(rightMax[i + ], height[i]);
    }
    
       ;
     (   ; i < len; ++i) {
        
        ans += Math.min(leftMax[i], rightMax[i]) - height[i];
    }
     ans;
}
//记录第 i 根柱子右边(包括自身)中最高柱子的高度
int
new
int
1
1
for
int
i
=
2
0
1
//把计算接水总面积拆分成计算单个柱子的接水面积
int
ans
=
0
for
int
i
=
0
//当前柱子接水面积 = (min(左高,右高) - 自身高度)*1
return

三、双指针

3.1 空间复杂度的优化

动态规划的做法中,需要维护两个数组 leftMax 和 rightMax,因此空间复杂度是 O(n)。

注意到下标 i 处能接的雨水量由 leftMax[i] 和 rightMax[i] 中的最小值决定。动态规划里面我们采用先统一计算左高右高,再计算面积。那我们可不可以做到在计算左高右高的同时计算面积,这样计算过面积的柱子的左高右高就可以被释放,不用存储在数组里面,我们采用指针来代替。

3.2 问题的解决

但是现在问题来了,我们的 leftMax 是从左往右计算的,而 rightMax 是从右往左计算的,假设我们要从左边开始处理柱子的接水面积,那么我们不用担心 leftMax,因为 leftMax 的计算方向和我们处理柱子的方向一致,可是 rightMax 我们就要遍历一整个数组来得出,并且我们向右移动柱子时又要重新计算 rightMax,时间复杂度反而会提高,这该怎么办?

  • 我们再来回顾一下计算接水面积的公式**当前柱子接水面积 = (min(左高,右高) - 自身高度)1*

  • 如果我们在从左往右处理时能够确定左高=min(左高,右高),那么我们就不需要去遍历一次数组得出右高了。

  • 我们可以明确确定的是左指针的 leftMax,和右指针的 rightMax,因为指针移动方向和计算高度最值方向一致

  • 当左指针的 leftMax<右指针的 rightMax , 左指针的 leftMax=min(左指针的 leftMax,左指针的 rightMax),因为右指针的 rightMax<=左指针的 rightMax

  • 当左指针的 leftMax>=右指针的 rightMax , 右指针的 rightMax=min(右指针的 leftMax,右指针的 rightMax),因为右指针的 leftMax>=左指针的 leftMax

3.3 代码的实现

/**
 * 双指针
 * @param height 柱子的高度
 * @return 接住雨水的总数
 */
public int trap(int[] height) {
    int len = height.length;
    // 当柱子个数小于三时,无法接到雨水
    // 接雨水的条件是两边的比中间高,所以至少要有三个柱子
    if (len < 3) {
        return 0;
    }
    //定义左右指针
    int left = 0, right = height.length - 1;
    /**
     * leftMax 是左指针指向柱子左边(包括自身)中最高柱子的高度
     * rightMax 是右指针指向柱子右边(包括自身)中最高柱子的高度
     * 我们可以明确确定的是左指针的 leftMax,和右指针的 rightMax
     * 因为指针移动方向和计算高度最值方向一致
     */
    int leftMax = 0, rightMax = 0;
    //把计算接水总面积拆分成计算单个柱子的接水面积
    int ans = 0;
    while (left < right) {
        leftMax = Math.max(leftMax, height[left]);
        rightMax = Math.max(rightMax, height[right]);
        //当前柱子接水面积 = (min(左高,右高) - 自身高度)*1
        if (leftMax < rightMax) {
            //左指针的 leftMax=min(左指针的 leftMax,左指针的 rightMax)
            //因为右指针的 rightMax<=左指针的 rightMax
            ans += leftMax - height[left];
            ++left;
        } else {
            //右指针的 rightMax=min(右指针的 leftMax,右指针的 rightMax)
            //因为右指针的 leftMax>=左指针的 leftMax
            ans += rightMax - height[right];
            --right;
        }
    }
    return ans;
}

3.4 指针的停止

我们从代码中可知,leftMax < rightMax 移动左指针,leftMax >= rightMax 移动右指针,最终会有一个指针到达最高处后停止不动,等待另外一个指针过来,直到 left == right,那么当 left == right 时已经退出了循环,此时指向的柱子的面积还没有计算,但是没有关系,因为在最高柱子上 height[left] = height[right] = leftMax = rightMax,算出来的面积也是 0,所以算不算加不加都无所谓,就算把循环条件换成 left <= right 也没有事,只是不需要多此一举。

3.5 移动指针的条件

在官方给出的题解中,移动指针的条件是 height[left] < height[right],并给出了如果 height[left]<height[right],则必有 leftMax<rightMax 的结论,我们来证明一下。

前提条件:

  • height[left]<=leftMax
  • height[right]<=rightMax

反证:

  • 假设 height[left] = 165 , leftMax = 180 , height[right] = 170 , rightMax= 170
  • 显然这种情况是不可能存在的,因为 180 指向的值肯定在 left 左侧,如果 height[right] 小于等于 180,会一直移动找到一个大于 180 的地方停下,所以停下的时候 leftMax<rightMax

就也说我们刚开始都是左指针指向了 leftMax,右指针指向 rightMax,然后小的一方开始移动,大的一方保持当前的最值,小的一方会一直移动直到超过大的一方,停在当前最值,换另一方移动。

总的来说,刚开始两方都保持 height[left]==leftMax,height[right]==rightMax

  • 如果是 height[left] < height[right],也就是 leftMax<rightMax,然后开始移动左指针,移动后有两种可能:
    • height[left] < height[right] ,此时 leftMax 要么保持原样,leftMax<rightMax,要么 leftMax 更新,leftMax = height[left] < height[right]=rightMax,leftMax<rightMax
    • height[left] >= height[right],此时 leftMax = height[left] >= height[right]=rightMax ,leftMax>=rightMax
  • 如果是 height[left] >= height[right],也就是 leftMax>=rightMax,然后开始移动右指针,移动后有两种可能:
    • height[left] >= height[right] ,此时 rightMax 要么保持原样,leftMax>=rightMax,要么 leftMax 更新,leftMax = height[left] >= height[right]=rightMax,leftMax>=rightMax
    • height[left] < height[right],此时 leftMax = height[left] < height[right]=rightMax, leftMin<rightMax

所以如果 height[left]<height[right],则必有 leftMax<rightMax,如果 height[left] >=height[right],则必有 leftMax>=rightMax

/**
 * 双指针
 * @param height 柱子的高度
 * @return 接住雨水的总数
 */
public int trap(int[] height) {
    int len = height.length;
    // 当柱子个数小于三时,无法接到雨水
    // 接雨水的条件是两边的比中间高,所以至少要有三个柱子
    if (len < 3) {
        return 0;
    }
    //定义左右指针
    int left = 0, right = height.length - 1;
    /**
     * leftMax 是左指针指向柱子左边(包括自身)中最高柱子的高度
     * rightMax 是右指针指向柱子右边(包括自身)中最高柱子的高度
     * 我们可以明确确定的是左指针的 leftMax,和右指针的 rightMax
     * 因为指针移动方向和计算高度最值方向一致
     */
    int leftMax = 0, rightMax = 0;
    //把计算接水总面积拆分成计算单个柱子的接水面积
    int ans = 0;
    while (left < right) {
        leftMax = Math.max(leftMax, height[left]);
        rightMax = Math.max(rightMax, height[right]);
        //当前柱子接水面积 = (min(左高,右高) - 自身高度)*1
        //现在要么是 height[left] == leftMax,要么是 height[right] == rightMax
        if (height[left] < height[right]) {
            /**
             * 进入此处有三种情况:
             * 1.height[left] == leftMax and height[right] == rightMax
             * height[left] < height[right] => leftMax< rightMax
             * 2.height[right] == rightMax 并且 leftMax< rightMax 移动左指针后
             * height[left] < height[right] ,此时 leftMax 要么保持原样 => leftMax< rightMax
             * 要么 leftMax 更新,leftMax = height[left] < height[right] = rightMax => leftMax< rightMax
             * 3.height[left] == leftMax 并且 leftMax>=rightMax 移动右指针后
             * height[left] < height[right],此时 leftMax = height[left] < height[right]=rightMax => leftMax< rightMax
             */
            //左指针的 leftMax=min(左指针的 leftMax,左指针的 rightMax)
            //因为右指针的 rightMax<=左指针的 rightMax
            ans += leftMax - height[left];
            ++left;
        } else {
            /**
             * 进入此处有三种情况:
             * 1.height[left] == leftMax and height[right] == rightMax
             * height[left] >= height[right] => leftMax>= rightMax
             * 2.height[right] == rightMax 并且 leftMax< rightMax 移动左指针后
             * height[left] >= height[right],此时 leftMax = height[left] >= height[right]=rightMax => leftMax>= rightMax
             * 3.height[left] == leftMax 并且 leftMax>=rightMax 移动右指针后
             * height[left] >= height[right] ,此时 rightMax 要么保持原样 => leftMax>= rightMax
             * 要么 rightMax 更新,leftMax = height[left] >= height[right] = rightMax => leftMax>= rightMax
             */
            //右指针的 rightMax=min(右指针的 leftMax,右指针的 rightMax)
            //因为右指针的 leftMax>=左指针的 leftMax
            ans += rightMax - height[right];
            --right;
        }
    }
    return ans;
}

四、单调栈

4.1 面积计算

我们上面两种解法都是**当前柱子接水面积 = (min(左高,右高) - 自身高度)1*,是纵向的,那么现在我们来思考一下横向该怎么计算?

凹槽接水面积 =( min(左边界高度,右边界高度)- 底部高度) 底部宽度*

4.2 栈的思想

如果我们要依次计算的话,从左边开始,记录 1 号的左边界 -> 1 号的右边界 ->1 号处理完毕-> 2 号的左边界-> 3 号的左边界-> 3 号的右边界 - >3 号处理完毕->2 号的右边界->2 号处理完毕->4 号的左边界-> 4 号的右边界->4 号处理完毕

我们可以清晰看到 3 号比 2 号先处理完毕,后进先出,这是栈的思想。

4.3 入栈和出栈

首先入栈的都是作为凹槽的底部,每个元素都有可能会作为底部,所以每个元素都要入栈。

当当前元素值大于栈顶,那么就取出栈顶的元素作为凹槽的底部,当前元素作为凹槽的右边界,左边界则是取出元素后栈的顶部元素,左边界一定是比底部高的,因为如果左边界比底部低会被取出来形成一个凹槽,计算完当前凹槽面积后,若栈为空,或当前元素小于栈顶元素,右边界入栈,若仍然大于栈顶元素则继续计算凹槽

4.4 代码实现

/**
 * 单调栈
 * @param height 柱子的高度
 * @return 接住雨水的总数
 */
public int trap(int[] height) {
    int ans = 0;
    //栈中存放凹槽的底部
    Deque<Integer> stack = new LinkedList<>();
    int n = height.length;
    for (int i = 0; i < n; ++i) {
        //当栈中有元素,并且栈顶元素小于当前元素,可以作为凹槽的底部
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            //取出栈顶元素作为凹槽底部
            int top = stack.pop();
            //如果栈中没有元素,那么就没有元素可以作为凹槽的左边界,无法成功接雨水,退出循环
            if (stack.isEmpty()) {
                break;
            }
            //获取栈顶元素作为栈的左边界,左边界是一定大于底部的
            //因为如果底部大于左边界,那么就会形成一个凹槽,在处理当前元素之前已经处理过了
            //所以栈里面的元素肯定是越靠近栈底元素越大,这也就是单调栈
            int left = stack.peek();
            //计算凹槽底部
            int currWidth = i - left - 1;
            //计算凹槽高度
            int currHeight = Math.min(height[left], height[i]) - height[top];
            //凹槽接水面积 =( min(左边界高度,右边界高度)- 底部高度)* 底部宽度
            ans += currWidth * currHeight;
        }
        //当前元素作为右边界计算凹槽的面积已经计算完毕
        //当前元素作为底部压入栈中
        stack.push(i);
    }
    return ans;
}

五、总结

解法时间复杂度空间复杂度核心思想计算方式
动态规划O(n)O(n)预存左右最大高度,直接计算每个位置的接水量纵向
双指针O(n)O(1)边遍历边更新最大高度,用'谁小移谁'优化空间纵向
单调栈O(n)O(n)维护递减栈,横向计算每个凹槽的存水面积横向

目录

  1. 一、引言
  2. 二、动态规划
  3. 2.1 解题步骤
  4. 2.2 动态规划的使用
  5. 2.3 代码实现
  6. 三、双指针
  7. 3.1 空间复杂度的优化
  8. 3.2 问题的解决
  9. 3.3 代码的实现
  10. 3.4 指针的停止
  11. 3.5 移动指针的条件
  12. 四、单调栈
  13. 4.1 面积计算
  14. 4.2 栈的思想
  15. 4.3 入栈和出栈
  16. 4.4 代码实现
  17. 五、总结

更多推荐文章

查看全部
  • MetaRTC 跨平台 WebRTC SDK 开发入门指南
  • Ubuntu22.04 安装 ROS2 及 MoveIt2 实现运动规划
  • Python 行业现状与职业发展前景深度解析
  • AI 绘画 API 开发实战:基于 Z-Image-Turbo 构建图像生成服务
  • 使用 AI 工具(Cursor/VS Code)调试 MATLAB 代码实测
  • Linux 进程间通信进阶:管道与共享内存
  • Java 状态机详解:三种实现方式优雅消除 if-else 嵌套
  • Trae AI 辅助编程实战指南:从入门到提效
  • 普通人兼职做自媒体的真实收入与现状分析
  • Python 编程实战练习:第 171 至 180 题详解
  • 使用 Llama-Factory 微调数学解题模型的思维链优化
  • 美团大模型应用算法工程师岗位职责与技术栈详解
  • Hello World 背后的启动逻辑
  • 基于 Django 与 Python 的电商用户行为分析系统设计与实现
  • Windows 10/11 部署 OpenClaw 机器人控制框架指南
  • FPGA 实现 UART 串口通信详解
  • 纯前端 JavaScript 通过 IP 地址获取用户地理位置
  • iOS 网络安全实战:HTTPS 通信与证书锁定防抓包
  • Python 接口、抽象基类与协议详解
  • Redis Cluster 哈希槽分片机制与 Linux 集群搭建实操

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online