LeetCode 42:接雨水——从当前位置推导左右两侧最大高度

LeetCode 42:接雨水——从当前位置推导左右两侧最大高度

一、题目描述

今天做的是 LeetCode 42:接雨水

题目给定一个数组:
在这里插入图片描述

height = [0,1,0,2,1,0,1,3,2,1,2,1]

数组中的每个数字表示柱子的高度。

下雨之后,柱子之间可以存储一定的雨水,需要计算最终能够接到的雨水总量。

这道题最开始看起来比较直观,但真正写代码时,需要解决一个问题:

对于每一个位置,到底能够接多少水?


二、先考虑一个位置能够接多少水

假设现在看第 i 个柱子:

左边最高柱子       右边最高柱子
     ↓                  ↓
     █                  █
     █       ↓          █
     █       █          █

这个位置最终能装多少水,取决于左右两边较矮的那一堵墙。

所以可以得到:

当前位置水量
=
min(左边最高高度, 右边最高高度)
-
当前柱子高度

也就是:

Math.min(max_left[i], max_right[i]) - height[i]

这里的关键就是:

我们需要知道每个位置左边的最大高度,以及右边的最大高度。


三、第一步:记录每个位置左边的最大高度

我定义:

int[] max_left = new int[len];

其中:

max_left[i]

表示:

从数组开头到 i 位置为止,出现过的最大高度。

例如:

height =
[0, 1, 0, 2, 1]

那么:

max_left =
[0, 1, 1, 2, 2]

计算方式非常简单:

max_left[0] = height[0];

for(int i = 1; i < len; i++){
    max_left[i] = Math.max(height[i], max_left[i - 1]);
}

例如计算 max_left[3]

height[3] = 2
max_left[2] = 1

所以:

max_left[3] = max(2, 1)
            = 2

这样遍历一次,就可以提前把每个位置左侧的最高柱子记录下来。


四、第二步:记录每个位置右边的最大高度

同样的道理,再定义:

int[] max_right = new int[len];

其中:

max_right[i]

表示:

i 位置到数组末尾为止,出现过的最大高度。

这一次需要从右向左遍历:

max_right[len - 1] = height[len - 1];

for(int i = len - 2; i >= 0; i--){
    max_right[i] = Math.max(height[i], max_right[i + 1]);
}

例如:

height =
[0,1,0,2,1]

计算得到:

max_right =
[2,2,2,2,1]

现在,对于每一个位置:

左边最高值
+
右边最高值
+
当前高度

我们都已经可以快速得到。


五、第三步:计算每个位置能够接多少水

有了:

max_left[i]
max_right[i]

那么当前位置的水量就是:

Math.min(max_left[i], max_right[i]) - height[i]

为什么是 min

因为如果左边最高柱子是:

5

右边最高柱子是:

3

那么水最多只能到:

3

的位置。

如果水超过右边高度:

左边:5
右边:3

█████
█████
██水█
██水█

水就会从右边流出去。

所以决定水位的是:

左右最高柱子中较矮的那个

也就是:

Math.min(max_left[i], max_right[i])

最后减去当前柱子的高度:

Math.min(max_left[i], max_right[i]) - height[i]

就是这个位置实际能够存储的水量。


六、我的最终代码

class Solution {
    public int trap(int[] height) {

        int len = height.length;

        // 记录每个位置左侧的最大高度
        int[] max_left = new int[len];
        max_left[0] = height[0];

        // 记录每个位置右侧的最大高度
        int[] max_right = new int[len];
        max_right[len - 1] = height[len - 1];

        // 从左向右计算左侧最大值
        for(int i = 1; i < len; i++){
            max_left[i] = Math.max(
                height[i],
                max_left[i - 1]
            );
        }

        // 从右向左计算右侧最大值
        for(int i = len - 2; i >= 0; i--){
            max_right[i] = Math.max(
                height[i],
                max_right[i + 1]
            );
        }

        // 计算每个位置能够接的雨水
        int res = 0;

        for(int i = 0; i < len; i++){
            res += Math.min(
                max_left[i],
                max_right[i]
            ) - height[i];
        }

        return res;
    }
}

七、复杂度分析

代码一共进行了三次遍历:

第一次:计算 max_left
第二次:计算 max_right
第三次:计算答案

每次都是 O(n)

所以总时间复杂度仍然是:

O(n)

不过额外维护了两个数组:

int[] max_left
int[] max_right

所以空间复杂度为:

O(n)

八、总结

这道题我最终采用的是左右最大值数组的思路。

核心并不是模拟雨水,而是把问题拆成:

对于每个位置 i:

左边最高是多少?
右边最高是多少?

然后:

Math.min(max_left[i], max_right[i]) - height[i]

就是当前位置可以接到的雨水。

最终:

时间复杂度:O(n)
空间复杂度:O(n)

这道题让我比较明显地感受到:

很多看起来需要“模拟过程”的题目,实际上可以换一个角度,直接计算最终状态。

这里没有去模拟雨水从哪里流到哪里,而是直接计算:

每个位置最终能够存多少水。

然后把所有位置的结果加起来,就得到了最终答案。

下一步如果继续优化,就是尝试使用双指针把空间复杂度降到 O(1)。这也会是这道题更经典的一种解法。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

find1star

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值