接雨水.md 17.4 KB
Newer Older
L
labuladong 已提交
1 2
---
title: '接雨水问题详解'
L
labuladong 已提交
3
tags: ['设计', '双指针']
L
labuladong 已提交
4
---
L
labuladong 已提交
5

L
labuladong 已提交
6
<!-- [手把手搞懂接雨水问题的多种解法](https://mp.weixin.qq.com/s/8E2WHPdArs3KwSwaxFunHw) -->
L
labuladong 已提交
7
 
8 9
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
10
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
11 12 13 14
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
15
![](https://labuladong.github.io/pictures/souyisou1.png)
16

L
labuladong 已提交
17
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线。[第 18 期每日打卡](https://aep.xet.tech/s/2PLO1n) 开始报名。反馈或修正 chatGPT 翻译的多语言代码 [点击这里](https://github.com/labuladong/fucking-algorithm/issues/1113)。另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
18 19


L
labuladong 已提交
20 21 22 23 24 25 26

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [11. Container With Most Water](https://leetcode.com/problems/container-with-most-water/) | [11. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/) | 🟠
| [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/) | [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/) | 🔴
27 28 29

**-----------**

L
labuladong 已提交
30
力扣第 42 题「接雨水」挺有意思,在面试题中出现频率还挺高的,本文就来步步优化,讲解一下这道题。
L
labuladong 已提交
31 32 33

先看一下题目:

L
labuladong 已提交
34
![](https://labuladong.github.io/pictures/接雨水/title.png)
L
labuladong 已提交
35 36 37

就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。

L
labuladong 已提交
38
<!-- muliti_language -->
L
labuladong 已提交
39 40 41 42 43 44 45 46
```java
int trap(int[] height);
```

下面就来由浅入深介绍暴力解法 -> 备忘录解法 -> 双指针解法,在 O(N) 时间 O(1) 空间内解决这个问题。

### 一、核心思路

47
所以对于这种问题,我们不要想整体,而应该去想局部;就像之前的文章写的动态规划问题处理字符串问题,不要考虑如何处理整个字符串,而是去思考应该如何处理每一个字符。
L
labuladong 已提交
48

49
这么一想,可以发现这道题的思路其实很简单。具体来说,仅仅对于位置 `i`,能装下多少水呢?
L
labuladong 已提交
50

L
labuladong 已提交
51
![](https://labuladong.github.io/pictures/接雨水/0.jpg)
L
labuladong 已提交
52

53
能装 2 格水,因为 `height[i]` 的高度为 0,而这里最多能盛 2 格水,2-0=2。
L
labuladong 已提交
54

L
labuladong 已提交
55
为什么位置 `i` 最多能盛 2 格水呢?因为,位置 `i` 能达到的水柱高度和其左边的最高柱子、右边的最高柱子有关,我们分别称这两个柱子高度为 `l_max``r_max`**位置 i 最大的水柱高度就是 `min(l_max, r_max)`**
L
labuladong 已提交
56

57
更进一步,对于位置 `i`,能够装的水为:
L
labuladong 已提交
58 59 60 61 62 63 64 65 66 67 68

```python
water[i] = min(
               # 左边最高的柱子
               max(height[0..i]),  
               # 右边最高的柱子
               max(height[i..end]) 
            ) - height[i]
    
```

L
labuladong 已提交
69
![](https://labuladong.github.io/pictures/接雨水/1.jpg)
L
labuladong 已提交
70

L
labuladong 已提交
71
![](https://labuladong.github.io/pictures/接雨水/2.jpg)
L
labuladong 已提交
72 73 74

这就是本问题的核心思路,我们可以简单写一个暴力算法:

L
labuladong 已提交
75
<!-- muliti_language -->
L
labuladong 已提交
76 77 78
```java
int trap(int[] height) {
    int n = height.length;
79
    int res = 0;
L
labuladong 已提交
80 81 82 83
    for (int i = 1; i < n - 1; i++) {
        int l_max = 0, r_max = 0;
        // 找右边最高的柱子
        for (int j = i; j < n; j++)
L
labuladong 已提交
84
            r_max = Math.max(r_max, height[j]);
L
labuladong 已提交
85 86
        // 找左边最高的柱子
        for (int j = i; j >= 0; j--)
L
labuladong 已提交
87
            l_max = Math.max(l_max, height[j]);
L
labuladong 已提交
88 89
        // 如果自己就是最高的话,
        // l_max == r_max == height[i]
L
labuladong 已提交
90
        res += Math.min(l_max, r_max) - height[i];
L
labuladong 已提交
91
    }
92
    return res;
L
labuladong 已提交
93 94 95 96 97 98 99
}
```

有之前的思路,这个解法应该是很直接粗暴的,时间复杂度 O(N^2),空间复杂度 O(1)。但是很明显这种计算 `r_max``l_max` 的方式非常笨拙,一般的优化方法就是备忘录。

### 二、备忘录优化

100
之前的暴力解法,不是在每个位置 `i` 都要计算 `r_max``l_max` 吗?我们直接把结果都提前计算出来,别傻不拉几的每次都遍历,这时间复杂度不就降下来了嘛。
L
labuladong 已提交
101

102
**我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**。预先把这两个数组计算好,避免重复计算:
L
labuladong 已提交
103

L
labuladong 已提交
104
<!-- muliti_language -->
L
labuladong 已提交
105
```java
L
labuladong 已提交
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
class Solution {
    int trap(int[] height) {
        if (height.length == 0) {
            return 0;
        }
        int n = height.length;
        int res = 0;
        // 数组充当备忘录
        int[] l_max = new int[n];
        int[] r_max = new int[n];
        // 初始化 base case
        l_max[0] = height[0];
        r_max[n - 1] = height[n - 1];
        // 从左向右计算 l_max
        for (int i = 1; i < n; i++)
            l_max[i] = Math.max(height[i], l_max[i - 1]);
        // 从右向左计算 r_max
        for (int i = n - 2; i >= 0; i--)
            r_max[i] = Math.max(height[i], r_max[i + 1]);
        // 计算答案
        for (int i = 1; i < n - 1; i++)
            res += Math.min(l_max[i], r_max[i]) - height[i];
        return res;
L
labuladong 已提交
129
    }
L
labuladong 已提交
130 131 132
}
```

133
这个优化其实和暴力解法思路差不多,就是避免了重复计算,把时间复杂度降低为 O(N),已经是最优了,但是空间复杂度是 O(N)。下面来看一个精妙一些的解法,能够把空间复杂度降低到 O(1)。
L
labuladong 已提交
134 135 136 137 138 139 140

### 三、双指针解法

这种解法的思路是完全相同的,但在实现手法上非常巧妙,我们这次也不要用备忘录提前计算了,而是用双指针**边走边算**,节省下空间复杂度。

首先,看一部分代码:

L
labuladong 已提交
141
<!-- muliti_language -->
L
labuladong 已提交
142 143 144 145
```java
int trap(int[] height) {
    int left = 0, right = height.length - 1;
    int l_max = 0, r_max = 0;
L
labuladong 已提交
146
    
L
labuladong 已提交
147 148 149 150
    while (left < right) {
        l_max = Math.max(l_max, height[left]);
        r_max = Math.max(r_max, height[right]);
        // 此时 l_max 和 r_max 分别表示什么?
L
labuladong 已提交
151 152 153 154 155 156 157 158 159 160 161
        left++; right--;
    }
}
```

对于这部分代码,请问 `l_max``r_max` 分别表示什么意义呢?

很容易理解,**`l_max` 是 `height[0..left]` 中最高柱子的高度,`r_max` 是 `height[right..end]` 的最高柱子的高度**

明白了这一点,直接看解法:

L
labuladong 已提交
162
<!-- muliti_language -->
L
labuladong 已提交
163
```java
L
labuladong 已提交
164 165 166 167
class Solution {
    int trap(int[] height) {
        int left = 0, right = height.length - 1;
        int l_max = 0, r_max = 0;
L
labuladong 已提交
168

L
labuladong 已提交
169 170 171 172 173 174 175 176 177 178 179 180 181
        int res = 0;
        while (left < right) {
            l_max = Math.max(l_max, height[left]);
            r_max = Math.max(r_max, height[right]);

            // res += min(l_max, r_max) - height[i]
            if (l_max < r_max) {
                res += l_max - height[left];
                left++;
            } else {
                res += r_max - height[right];
                right--;
            }
L
labuladong 已提交
182
        }
L
labuladong 已提交
183
        return res;
L
labuladong 已提交
184 185 186 187 188 189
    }
}
```

你看,其中的核心思想和之前一模一样,换汤不换药。但是细心的读者可能会发现次解法还是有点细节差异:

190
之前的备忘录解法,`l_max[i]``r_max[i]` 分别代表 `height[0..i]``height[i..end]` 的最高柱子高度。
L
labuladong 已提交
191

L
labuladong 已提交
192 193
```java
res += Math.min(l_max[i], r_max[i]) - height[i];
L
labuladong 已提交
194 195
```

L
labuladong 已提交
196
![](https://labuladong.github.io/pictures/接雨水/3.jpg)
L
labuladong 已提交
197 198 199

但是双指针解法中,`l_max``r_max` 代表的是 `height[0..left]``height[right..end]` 的最高柱子高度。比如这段代码:

L
labuladong 已提交
200
```java
L
labuladong 已提交
201
if (l_max < r_max) {
202
    res += l_max - height[left];
L
labuladong 已提交
203 204 205 206
    left++; 
} 
```

L
labuladong 已提交
207
![](https://labuladong.github.io/pictures/接雨水/4.jpg)
L
labuladong 已提交
208 209 210

此时的 `l_max``left` 指针左边的最高柱子,但是 `r_max` 并不一定是 `left` 指针右边最高的柱子,这真的可以得到正确答案吗?

211
其实这个问题要这么思考,我们只在乎 `min(l_max, r_max)`**对于上图的情况,我们已经知道 `l_max < r_max` 了,至于这个 `r_max` 是不是右边最大的,不重要。重要的是 `height[i]` 能够装的水只和较低的 `l_max` 之差有关**
L
labuladong 已提交
212

L
labuladong 已提交
213
![](https://labuladong.github.io/pictures/接雨水/5.jpg)
L
labuladong 已提交
214

215
这样,接雨水问题就解决了。
L
labuladong 已提交
216

L
labuladong 已提交
217 218 219 220
### 扩展延伸

下面我们看一道和接雨水问题非常类似的题目,力扣第 11 题「盛最多水的容器」:

L
labuladong 已提交
221
![](https://labuladong.github.io/pictures/接雨水/title1.png)
L
labuladong 已提交
222 223 224

函数签名如下:

L
labuladong 已提交
225
<!-- muliti_language -->
L
labuladong 已提交
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
```java
int maxArea(int[] height);
```

这题和接雨水问题很类似,可以完全套用前文的思路,而且还更简单。两道题的区别在于:

**接雨水问题给出的类似一幅直方图,每个横坐标都有宽度,而本题给出的每个横坐标是一条竖线,没有宽度**

我们前文讨论了半天 `l_max``r_max`,实际上都是为了计算 `height[i]` 能够装多少水;而本题中 `height[i]` 没有了宽度,那自然就好办多了。

举个例子,如果在接雨水问题中,你知道了 `height[left]``height[right]` 的高度,你能算出 `left``right` 之间能够盛下多少水吗?

不能,因为你不知道 `left``right` 之间每个柱子具体能盛多少水,你得通过每个柱子的 `l_max``r_max` 来计算才行。

反过来,就本题而言,你知道了 `height[left]``height[right]` 的高度,能算出 `left``right` 之间能够盛下多少水吗?

可以,因为本题中竖线没有宽度,所以 `left``right` 之间能够盛的水就是:

```python
min(height[left], height[right]) * (right - left)
```

类似接雨水问题,高度是由 `height[left]``height[right]` 较小的值决定的。

解决这道题的思路依然是双指针技巧:

**用 `left` 和 `right` 两个指针从两端向中心收缩,一边收缩一边计算 `[left, right]` 之间的矩形面积,取最大的面积值即是答案**

先直接看解法代码吧:

L
labuladong 已提交
256
<!-- muliti_language -->
L
labuladong 已提交
257
```java
L
labuladong 已提交
258 259 260 261 262 263 264 265 266 267 268 269 270 271
class Solution {
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1;
        int res = 0;
        while (left < right) {
            // [left, right] 之间的矩形面积
            int cur_area = Math.min(height[left], height[right]) * (right - left);
            res = Math.max(res, cur_area);
            // 双指针技巧,移动较低的一边
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
L
labuladong 已提交
272
        }
L
labuladong 已提交
273
        return res;
L
labuladong 已提交
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    }
}
```

代码和接雨水问题大致相同,不过肯定有读者会问,下面这段 if 语句为什么要移动较低的一边:

```java
// 双指针技巧,移动较低的一边
if (height[left] < height[right]) {
    left++;
} else {
    right--;
}
```

**其实也好理解,因为矩形的高度是由 `min(height[left], height[right])` 即较低的一边决定的**

你如果移动较低的那一边,那条边可能会变高,使得矩形的高度变大,进而就「有可能」使得矩形的面积变大;相反,如果你去移动较高的那一边,矩形的高度是无论如何都不会变大的,所以不可能使矩形的面积变得更大。

至此,这道题也解决了。

L
labuladong 已提交
295 296 297 298




299
**_____________**
E
eric496 已提交
300

L
labuladong 已提交
301
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
302

L
labuladong 已提交
303
![](https://labuladong.github.io/pictures/souyisou2.png)
L
labuladong 已提交
304 305


F
FanFan0919 已提交
306 307
======其他语言代码======

B
brucecat 已提交
308 309 310 311 312 313
[42.接雨水](https://leetcode-cn.com/problems/trapping-rain-water)



### java

F
FanFan0919 已提交
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
[Yifan Zhang](https://github.com/FanFan0919) 提供 java 代码

**双指针解法**:时间复杂度 O(N),空间复杂度 O(1)

对cpp版本的解法有非常微小的优化。  
因为我们每次循环只会选 left 或者 right 处的柱子来计算,因此我们并不需要在每次循环中同时更新`maxLeft``maxRight`
我们可以先比较 `maxLeft``maxRight`,决定这次选择计算的柱子是 `height[left]` 或者 `height[right]` 后再更新对应的 `maxLeft``maxRight`
当然这并不会在时间上带来什么优化,只是提供一种思路。

```java
class Solution {
    public int trap(int[] height) {
        if (height == null || height.length == 0) return 0;
        int left = 0, right = height.length - 1;
        int maxLeft = height[left], maxRight = height[right];
        int res = 0;
        
        while (left < right) {
            // 比较 maxLeft 和 maxRight,决定这次计算 left 还是 right 处的柱子
            if (maxLeft < maxRight) {
                left++;
                maxLeft = Math.max(maxLeft, height[left]);  // update maxLeft
                res += maxLeft - height[left];
            } else {
                right--;
                maxRight = Math.max(maxRight, height[right]);   // update maxRight
                res += maxRight - height[right];
            }
        }
        
        return res;
    }
}
```

附上暴力解法以及备忘录解法的 java 代码

**暴力解法**:时间复杂度 O(N^2),空间复杂度 O(1)  
```java
class Solution {
    public int trap(int[] height) {
        if (height == null || height.length == 0) return 0;
        int n = height.length;
        int res = 0;
        // 跳过最左边和最右边的柱子,从第二个柱子开始
        for (int i = 1; i < n - 1; i++) {
            int maxLeft = 0, maxRight = 0;
            // 找右边最高的柱子
            for (int j = i; j < n; j++) {
                maxRight = Math.max(maxRight, height[j]);
            }
            // 找左边最高的柱子
            for (int j = i; j >= 0; j--) {
                maxLeft = Math.max(maxLeft, height[j]);
            }
            // 如果自己就是最高的话,
            // maxLeft == maxRight == height[i]
            res += Math.min(maxLeft, maxRight) - height[i];
        }
        return res;
    }
}
```

**备忘录解法**:时间复杂度 O(N),空间复杂度 O(N)
```java
class Solution {
    public int trap(int[] height) {
        if (height == null || height.length == 0) return 0;
        int n = height.length;
        int res = 0;
        // 数组充当备忘录
        int[] maxLeft = new int[n];
        int[] maxRight = new int[n];
        // 初始化 base case 
        maxLeft[0] = height[0];
        maxRight[n - 1] = height[n - 1];
        
        // 从左向右计算 maxLeft
        for (int i = 1; i < n; i++) {
            maxLeft[i] = Math.max(maxLeft[i - 1], height[i]);
        }
        // 从右向左计算 maxRight
        for (int i = n - 2; i >= 0; i--) {
            maxRight[i] = Math.max(maxRight[i + 1], height[i]);
        }
        // 计算答案
        for (int i = 1; i < n; i++) {
            res += Math.min(maxLeft[i], maxRight[i]) - height[i];
        }
        return res;
    }
}
B
brucecat 已提交
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
```



### javascript

**暴力解法**

```js
/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function (height) {
    let n = height.length;
    let res = 0;

    for (let i = 1; i <= n - 2; i++) {
        let l_max = 0, r_max = 0;
        // 找右边高的柱子
        for (let j = i; j < n; j++) {
            r_max = Math.max(r_max, height[j])
        }

        // 找左边高的柱子
        for (let j = i; j >= 0; j--) {
            l_max = Math.max(l_max, height[j])
        }

        // 如果自己就是最高的话
        // l_max == r_max == height[i]
        res += Math.min(l_max, r_max) - height[i]
    }
    return res;
};
```



**备忘录优化**

```js
/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function (height) {
    let n = height.length;
    if (n <= 2) {
        return 0;
    }

    let res = 0;

    // 数组充当备忘录
    let l_max = new Array(n);
    let r_max = new Array(n);

    // 初始化base case
    l_max[0] = height[0];
    r_max[n - 1] = height[n - 1];

    // 从左往右算l_max
    for (let i = 1; i < n; i++) {
        l_max[i] = Math.max(height[i], l_max[i - 1])
    }

    // 从右往左计算r_max
    for (let i = n - 2; i >= 0; i--) {
        r_max[i] = Math.max(height[i], r_max[i + 1])
    }

    // 计算答案
    for (let i = 1; i <= n - 2; i++) {
        res += Math.min(l_max[i], r_max[i]) - height[i];
    }
    
    return res;
};
```



**双指针解法**

```js
/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function (height) {
    let n = height.length;
    if (n <= 2) {
        return 0;
    }

    let res = 0;

    let left = 0;
    let right = n - 1;

    let l_max = height[0];
    let r_max = height[n - 1];

    while (left <= right) {
        l_max = Math.max(l_max, height[left]);
        r_max = Math.max(r_max, height[right]);

        // res += min(l_max, r_max) - height[i]
        if (l_max < r_max) {
            res += l_max - height[left];
            left++;
        } else {
            res += r_max - height[right];
            right--;
        }
    }
    return res;
};
```