【LeetCode 热题 100】接雨水


精选专栏链接 🔗


欢迎订阅,点赞+关注,每日精进1%,与百万开发者共攀技术珠峰

更多内容持续更新中~



【LeetCode 热题 100】接雨水

📝题目描述

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

示例1

bash 复制代码
输入: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:

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

💡提示信息

  • n == height.length;
  • 1 <= n <= 2 ∗ 10 4 2 * 10^4 2∗104;
  • 0 <= heighti <= 10 5 10^5 105;

什么是木桶效应?

"木桶效应"(Cask Effect),也被称为"短板效应"或"木桶定律",是一个经典的管理学和系统论概念。简单来说,它的核心含义是:一只木桶能盛多少水,并不取决于最长的那块木板,而是取决于最短的那块木板。

这个理论由美国管理学家劳伦斯·J·彼得(Laurence J. Peter)提出,用以说明一个系统或组织的整体效能,往往受制于其最薄弱的环节。


解题思路解析

这道题的核心在于:对于数组中的任意一个位置 i,它能接到的雨水高度是多少?

根据木桶效应,位置 i 上方的水位高度,取决于它左侧最大高度右侧最大高度 中的较小值

公式如下:

water i = max ⁡ ( 0 , min ⁡ ( left_max i , right_max i ) − height i ) \text{water}i = \max(0, \min(\text{left\_max}i, \text{right\_max}i) - \text{height}i) wateri=max(0,min(left_maxi,right_maxi)−heighti)

  • left_max[i]:位置 i 左边(包含 i)最高的柱子高度;
  • right_max[i]:位置 i 右边(包含 i)最高的柱子高度;
  • height[i]:当前位置柱子的高度;
  • 如果计算结果小于 0,说明当前柱子高度比水位还高,接不到水,记为 0;

基于这个核心思路,有两种主要的实现方式。


解法一:动态规划 (预处理数组)

这是最直观的解法。我们可以预先计算每个位置的 left_maxright_max 并存储在数组中,这样在遍历时就可以 O ( 1 ) O(1) O(1) 时间获取。

算法步骤:

  1. 创建两个数组 leftMaxrightMax
  2. 从左向右扫描,填充 leftMaxleftMax[i] = max(leftMax[i-1], height[i])
  3. 从右向左扫描,填充 rightMaxrightMax[i] = max(rightMax[i+1], height[i])
  4. 最后遍历一次数组,累加每个位置的雨水量;

Java 代码实现

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

        int n = height.length;
        int[] leftMax = new int[n];
        int[] rightMax = new int[n];

        // 1. 填充左边的最大值
        leftMax[0] = height[0];
        for (int i = 1; i < n; i++) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        // 2. 填充右边的最大值
        rightMax[n - 1] = height[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

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

        return ans;
    }
}

提交代码,运行结果如下

复杂度分析

时间复杂度: O ( N ) O(N) O(N),我们需要遍历三次数组,每次都是 O ( N ) O(N) O(N)的时间复杂度;

空间复杂度: O ( N ) O(N) O(N),我们需要两个额外的数组来存储最大高度。


解法二:双指针 (最优解)

方法一虽然快,但占用 O ( N ) O(N) O(N) 的额外空间。我们可以通过双指针 技巧将空间复杂度优化到 O ( 1 ) O(1) O(1)。

算法思路:

我们维护两个指针 leftright,分别从数组的两端向中间移动。同时维护两个变量 left_maxright_max,分别记录 left 指针左边扫过的最大值和 right 指针右边扫过的最大值。

关键逻辑:

依然根据公式:

water i = max ⁡ ( 0 , min ⁡ ( left_max i , right_max i ) − height i ) \text{water}i = \max(0, \min(\text{left\_max}i, \text{right\_max}i) - \text{height}i) wateri=max(0,min(left_maxi,right_maxi)−heighti)

在移动过程中,我们总是处理较矮那一侧的指针。

  • 如果 height[left] < height[right]
    • 由于算法每次都只移动较矮的一侧,能走到现在这个 left,说明左边从来没有被右边更高的柱子挡住而停滞不前;
    • 这意味着左边目前的最大值 left_max 一定小于右边的某个值(这个值可能是当前的 height[right] 或者 right_max);
    • 根据木桶效应,当前位置 left 的水位高度完全由 left_max 决定(因为短板在左边);
    • 我们可以直接计算 left 位置的雨水,然后将 left 右移;
  • 反之,如果 height[left] >= height[right]
    • 同理,右边目前的最大值 right_max 是短板。
    • 我们可以直接计算 right 位置的雨水,然后将 right 左移。

关健难点理解: 为什么 height[left] < height[right]时,左边目前的最大值 left_max 一定小于右边的某个值?

双指针每次只移动较矮的一侧 。假设我们成功地将左指针从起始位置 0 一路移动到了当前位置 left,这意味着在之前的每一步中,每当左指针指向某个位置时,它都满足"比当前右指针矮"的条件,从而被允许向右移动。如果曾经出现过"左指针指向的柱子比右指针高"的情况,那么算法就会去移动右指针,而左指针就会停在原地等待。

所以,左指针从未因为自己比右边高而被卡住;它每次都是因为自己比右边矮才得以向前移动。换句话说,右边始终有一个不低于左指针当前高度的墙(右指针位置)存在,才使得左指针能够不断前进。

height[left] < height[right] 成立时,左指针能走到当前位置,意味着在之前的每一步中,左指针指向的柱子始终比当时的右指针矮

Java 代码实习如下:

java 复制代码
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 ans = 0;

        while (left < right) {
            // 更新当前遍历过的最大值
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);

            if (height[left] < height[right]) {
                // 左边矮,处理左边
                // 此时 leftMax 一定小于 rightMax,所以水位由 leftMax 决定
                ans += leftMax - height[left];
                left++;
            } else {
                // 右边矮(或相等),处理右边
                // 此时 rightMax 一定小于等于 leftMax,水位由 rightMax 决定
                ans += rightMax - height[right];
                right--;
            }
        }

        return ans;
    }
}

提交代码,运行结果如下:

复杂度分析

时间复杂度: O ( N ) O(N) O(N),双指针遍历一次数组;

空间复杂度: O ( 1 ) O(1) O(1),只需要常数级别的额外空间;


总结

方法 时间复杂度 空间复杂度 评价
动态规划 O ( N ) O(N) O(N) O ( N ) O(N) O(N) 逻辑简单,容易想到,适合面试时作为第一解法。
双指针 O ( N ) O(N) O(N) O ( 1 ) O(1) O(1) 最优解,对思维要求较高,体现了对"木桶效应"的深刻理解。

希望这篇解析能帮你彻底搞懂"接雨水"问题!

相关推荐
春日见1 小时前
5分钟入门强化学习之动态规划算法与实现
大数据·人工智能·python·算法·机器学习·计算机视觉
scx_link1 小时前
线性回归的总结:
算法·机器学习·线性回归
郝亚军1 小时前
IEEE 754 单精度浮点的SEM表示
开发语言·c++·算法
青山师2 小时前
动态规划算法深度解析:从状态转移方程到工业级优化
数据结构·算法·面试·动态规划·代理模式·java面试
zhangjw342 小时前
第15篇:Java多线程零基础入门,进程线程、线程创建方式、线程生命周期、线程安全彻底吃透
java·开发语言·面试
Raink老师2 小时前
【AI面试临阵磨枪-086】什么是 AI Agent Skill?与传统 Function Calling、Tool 的区别?
人工智能·面试·职场和发展
蝈理塘(/_\)大怨种2 小时前
类和对象 (上)
java·开发语言
黎阳之光2 小时前
数智透明·安全兜底|黎阳之光透明矿山,AI+数字孪生守护矿山生命线
人工智能·物联网·算法·安全·数字孪生
吴可可1232 小时前
控制弦高精度的样条离散化方法
算法