42. 接雨水 🟥
题目
leetcode 困难
题目
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例1:
输入: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
题解
思路
“前后缀数组”和“双指针”解法,它们都有一个共同的特征:“按列求水”(竖着算)。每一次计算的都是某单一根柱子上方能积攒的细长条水柱。
而单调栈解法的思路有着本质的区别:它是 “按层求水”(横着算)。每一次计算的是两堵墙之间形成的一个“横向的水洼”。
方法:前后缀数组/动态规划
解题思路
想象一下,你站在第 i 列柱子上,左右两边各有一堵很高的墙。
这块地方能积多少水,取决于左右两边最矮的那堵墙(木桶效应)。
- 找到左边最高的柱子,高度记作 leftMax。
- 找到右边最高的柱子,高度记作 rightMax。
- 水面的高度就是这两者里的较小值:Math.min(leftMax, rightMax)。
- 第 i 列实际上能积水的量,就是水面高度减去当前柱子本身的高度:
当前列积水量 = Math.min(leftMax, rightMax) - height[i]
复杂度
- 时间复杂度:O(n)
n 是数组 height 的长度。计算数组 leftMax 和 rightMax 的元素值各需要遍历数组 height 一次,计算能接的雨水总量还需要遍历一次。
- 空间复杂度:O(n)
n 是数组 height 的长度。需要创建两个长度为 n 的数组 leftMax 和 rightMax。
js
function trap(height) {
const n = height.length
if (n == 0) {
return 0
}
const leftMax = new Array(n).fill(0)
leftMax[0] = height[0]
for (let i = 1; i < n; ++i) {
leftMax[i] = Math.max(leftMax[i - 1], height[i])
}
console.log('leftMax', leftMax)
const rightMax = new Array(n).fill(0)
rightMax[n - 1] = height[n - 1]
for (let i = n - 2; i >= 0; --i) {
rightMax[i] = Math.max(rightMax[i + 1], height[i])
}
console.log('rightMax', rightMax)
let ans = 0
for (let i = 0; i < n; ++i) {
const minHeight = Math.min(leftMax[i], rightMax[i])
const trappedWater = minHeight - height[i]
console.log('i', i, 'minHeight', minHeight, 'trappedWater', trappedWater)
ans += trappedWater
}
return ans
}
console.log('🌰', trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))
console.log('------------------')
console.log('🌰', trap([4, 2, 0, 3, 2, 5]))
执行结果
leftMax [
0, 1, 1, 2, 2,
2, 2, 3, 3, 3,
3, 3
]
rightMax [
3, 3, 3, 3, 3,
3, 3, 3, 2, 2,
2, 1
]
i 0 minHeight 0 trappedWater 0
i 1 minHeight 1 trappedWater 0
i 2 minHeight 1 trappedWater 1
i 3 minHeight 2 trappedWater 0
i 4 minHeight 2 trappedWater 1
i 5 minHeight 2 trappedWater 2
i 6 minHeight 2 trappedWater 1
i 7 minHeight 3 trappedWater 0
i 8 minHeight 2 trappedWater 0
i 9 minHeight 2 trappedWater 1
i 10 minHeight 2 trappedWater 0
i 11 minHeight 1 trappedWater 0
🌰 6
------------------
leftMax [ 4, 4, 4, 4, 4, 5 ]
rightMax [ 5, 5, 5, 5, 5, 5 ]
i 0 minHeight 4 trappedWater 0
i 1 minHeight 4 trappedWater 2
i 2 minHeight 4 trappedWater 4
i 3 minHeight 4 trappedWater 1
i 4 minHeight 4 trappedWater 2
i 5 minHeight 5 trappedWater 0
🌰 9
方法:首尾双指针
复杂度
- 时间复杂度:O(n)
- 空间复杂度:O(1)
js
function trap(height) {
let ans = 0
let left = 0,
right = height.length - 1
let leftMax = 0,
rightMax = 0
while (left < right) {
leftMax = Math.max(leftMax, height[left])
rightMax = Math.max(rightMax, height[right])
console.log('♻️ left:', left, 'right:', right, 'leftMax:', leftMax, 'rightMax:', rightMax, 'height[left]:', height[left], 'height[right]:', height[right])
if (height[left] < height[right]) {
console.log('接水 leftMax - height[left]:', leftMax - height[left])
ans += leftMax - height[left]
++left
} else {
console.log('接水 rightMax - height[right]:', rightMax - height[right])
ans += rightMax - height[right]
--right
}
}
return ans
}
console.log('🌰', trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))
console.log('------------------')
console.log('🌰', trap([4, 2, 0, 3, 2, 5]))
执行结果
♻️ left: 0 right: 11 leftMax: 0 rightMax: 1 height[left]: 0 height[right]: 1
接水 leftMax - height[left]: 0
♻️ left: 1 right: 11 leftMax: 1 rightMax: 1 height[left]: 1 height[right]: 1
接水 rightMax - height[right]: 0
♻️ left: 1 right: 10 leftMax: 1 rightMax: 2 height[left]: 1 height[right]: 2
接水 leftMax - height[left]: 0
♻️ left: 2 right: 10 leftMax: 1 rightMax: 2 height[left]: 0 height[right]: 2
接水 leftMax - height[left]: 1
♻️ left: 3 right: 10 leftMax: 2 rightMax: 2 height[left]: 2 height[right]: 2
接水 rightMax - height[right]: 0
♻️ left: 3 right: 9 leftMax: 2 rightMax: 2 height[left]: 2 height[right]: 1
接水 rightMax - height[right]: 1
♻️ left: 3 right: 8 leftMax: 2 rightMax: 2 height[left]: 2 height[right]: 2
接水 rightMax - height[right]: 0
♻️ left: 3 right: 7 leftMax: 2 rightMax: 3 height[left]: 2 height[right]: 3
接水 leftMax - height[left]: 0
♻️ left: 4 right: 7 leftMax: 2 rightMax: 3 height[left]: 1 height[right]: 3
接水 leftMax - height[left]: 1
♻️ left: 5 right: 7 leftMax: 2 rightMax: 3 height[left]: 0 height[right]: 3
接水 leftMax - height[left]: 2
♻️ left: 6 right: 7 leftMax: 2 rightMax: 3 height[left]: 1 height[right]: 3
接水 leftMax - height[left]: 1
🌰 6
------------------
♻️ left: 0 right: 5 leftMax: 4 rightMax: 5 height[left]: 4 height[right]: 5
接水 leftMax - height[left]: 0
♻️ left: 1 right: 5 leftMax: 4 rightMax: 5 height[left]: 2 height[right]: 5
接水 leftMax - height[left]: 2
♻️ left: 2 right: 5 leftMax: 4 rightMax: 5 height[left]: 0 height[right]: 5
接水 leftMax - height[left]: 4
♻️ left: 3 right: 5 leftMax: 4 rightMax: 5 height[left]: 3 height[right]: 5
接水 leftMax - height[left]: 1
♻️ left: 4 right: 5 leftMax: 4 rightMax: 5 height[left]: 2 height[right]: 5
接水 leftMax - height[left]: 2
🌰 9
方法:单调栈
复杂度
- 时间复杂度:O(n)
n 是数组 height 的长度。从 0 到 n−1 的每个下标最多只会入栈和出栈各一次。
- 空间复杂度:O(n)
n 是数组 height 的长度。空间复杂度主要取决于栈空间,栈的大小不会超过 n。
js
function trap(height) {
let ans = 0
const stack = []
const n = height.length
for (let i = 0; i < n; ++i) {
console.log('♻️ i', i, 'height[i]', height[i], 'stack', stack.toString())
// 发现当前柱子比栈顶的柱子高,说明遇到“右边界”了,可以找坑算水了!
while (stack.length && height[i] > height[stack[stack.length - 1]]) {
// 1. 弹出栈顶,它是“坑底”
const bottomIdx = stack.pop()
console.log('🕳️ bottomIdx', bottomIdx)
// 如果弹出来之后栈空了,说明左边没有更高的墙挡着,兜不住水,直接退出循环
if (stack.length === 0) {
break
}
// 2. 此时的新栈顶,就是“左边界”
const leftIdx = stack[stack.length - 1]
// 3. 计算宽度:右边界 - 左边界 - 1 (中间夹着的距离)
const width = i - leftIdx - 1
// 4. 计算高度:左右边界较矮的那个,减去坑底的高度
const h = Math.min(height[leftIdx], height[i]) - height[bottomIdx]
console.log(
'leftIdx',
leftIdx,
'width',
width,
'h',
h,
'接💦:',
width * h,
)
// 5. 累加这一个横向层面的积水
ans += width * h
}
// 如果当前柱子比栈顶矮,或者上面一顿操作算完水了,就把当前柱子入栈,等待将来的右边界
stack.push(i)
}
return ans
}
console.log('🌰', trap([0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1]))
console.log('------------------')
console.log('🌰', trap([4, 2, 0, 3, 2, 5]))
执行结果
♻️ i 0 height[i] 0 stack
♻️ i 1 height[i] 1 stack 0
🕳️ bottomIdx 0
♻️ i 2 height[i] 0 stack 1
♻️ i 3 height[i] 2 stack 1,2
🕳️ bottomIdx 2
leftIdx 1 width 1 h 1 接💦: 1
🕳️ bottomIdx 1
♻️ i 4 height[i] 1 stack 3
♻️ i 5 height[i] 0 stack 3,4
♻️ i 6 height[i] 1 stack 3,4,5
🕳️ bottomIdx 5
leftIdx 4 width 1 h 1 接💦: 1
♻️ i 7 height[i] 3 stack 3,4,6
🕳️ bottomIdx 6
leftIdx 4 width 2 h 0 接💦: 0
🕳️ bottomIdx 4
leftIdx 3 width 3 h 1 接💦: 3
🕳️ bottomIdx 3
♻️ i 8 height[i] 2 stack 7
♻️ i 9 height[i] 1 stack 7,8
♻️ i 10 height[i] 2 stack 7,8,9
🕳️ bottomIdx 9
leftIdx 8 width 1 h 1 接💦: 1
♻️ i 11 height[i] 1 stack 7,8,10
🌰 6
------------------
♻️ i 0 height[i] 4 stack
♻️ i 1 height[i] 2 stack 0
♻️ i 2 height[i] 0 stack 0,1
♻️ i 3 height[i] 3 stack 0,1,2
🕳️ bottomIdx 2
leftIdx 1 width 1 h 2 接💦: 2
🕳️ bottomIdx 1
leftIdx 0 width 2 h 1 接💦: 2
♻️ i 4 height[i] 2 stack 0,3
♻️ i 5 height[i] 5 stack 0,3,4
🕳️ bottomIdx 4
leftIdx 3 width 1 h 1 接💦: 1
🕳️ bottomIdx 3
leftIdx 0 width 4 h 1 接💦: 4
🕳️ bottomIdx 0
🌰 9