目录
- [1. 问题核心理解](#1. 问题核心理解)
- [2. 解法详解](#2. 解法详解)
- [3. 完整代码实现(Python 3)](#3. 完整代码实现(Python 3))
- [4. 算法复杂度对比总结](#4. 算法复杂度对比总结)
- [5. 常见疑问解答](#5. 常见疑问解答)
1. 问题核心理解
题目描述 :
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
直观理解 :
想象一下,雨水会落在柱子之间的凹陷处。对于任意一个位置 i,它能存多少水,取决于它左边最高的柱子 和右边最高的柱子。
- 水往低处流,所以水位高度不能超过左右两侧最高柱子中较矮的那个(木桶效应)。
- 当前位置能接的水量 =
min(左边最高,右边最高) - 当前位置高度。 - 如果计算结果为负数,说明当前位置比边界高,接不到水,记为 0。
核心公式:
\[\text{water}[i] = \max(0, \min(\text{left_max}[i], \text{right_max}[i]) - \text{height}[i]) \]
下面我将详细讲解三种主流解法:动态规划 、双指针 (最优解)、单调栈。
2. 解法详解
方法一:动态规划(预处理数组)
思路 :
核心公式需要知道每个位置左右的最大值。暴力法是每次遍历都重新扫描左右,效率低。动态规划的核心思想是空间换时间:
- 创建两个数组
left_max和right_max。 left_max[i]存储位置i及其左边的最高柱子高度。right_max[i]存储位置i及其右边的最高柱子高度。- 最后遍历一次数组,利用公式累加雨水量。
优点 :逻辑最直观,容易理解。
缺点:需要 O(n) 的额外空间存储两个数组。
方法二:双指针法(最优解)⭐
思路 :
动态规划中,我们发现其实不需要存储整个数组。我们可以用两个指针 left 和 right 分别从两端向中间移动。
- 维护两个变量
left_max和right_max,分别表示当前位置左边和右边已遇到的最大高度。 - 关键逻辑 :比较
left_max和right_max。- 如果
left_max < right_max:说明左边是短板。对于left位置来说,右边肯定有一个比left_max更高的柱子(即当前的right_max),所以left位置的水位由left_max决定。计算完后,left指针右移。 - 如果
left_max >= right_max:说明右边是短板。对于right位置来说,左边肯定有一个比right_max更高的柱子,所以right位置的水位由right_max决定。计算完后,right指针左移。
- 如果
- 这样可以在一次遍历中完成计算,且不需要额外数组。
优点 :时间复杂度 O(n),空间复杂度 O(1),性能最优。
缺点:逻辑稍绕,需要理解"短板决定水位"的动态过程。
方法三:单调栈(按层计算)
思路 :
前两种方法是按"列"计算(竖着算每一列能接多少水),单调栈是按"层"计算(横着算每一层凹槽能接多少水)。
- 维护一个单调递减栈,栈中存储柱子的索引。
- 遍历柱子,当当前柱子高度
h大于栈顶柱子高度时,说明形成了一个凹槽(栈顶是底部,新栈顶是左边界,当前柱子是右边界)。 - 弹出栈顶作为底部,计算这一层凹槽的面积:
宽度 × (左右边界最小高度 - 底部高度)。 - 重复直到栈满足单调递减,然后将当前柱子入栈。
优点 :适合处理类似"寻找最近的大于/小于元素"的问题,思路独特。
缺点:代码逻辑相对复杂,空间复杂度 O(n)。
3. 完整代码实现(Python 3)
以下代码包含了上述三种解法。主函数 trap 使用了最优的双指针法 。为了方便学习,我也添加了 trap_dp(动态规划)和 trap_stack(单调栈)方法。
python
from typing import List
class Solution:
def trap(self, height: List[int]) -> int:
"""
主解法:双指针法 (Two Pointers)
时间复杂度:O(n)
空间复杂度:O(1)
这是面试中最推荐的解法,因为它在时间和空间上都是最优的。
"""
# 边界条件:如果柱子数量小于 3,无法形成凹槽接水
if not height or len(height) < 3:
return 0
# 初始化左右指针,分别指向数组的头和尾
left, right = 0, len(height) - 1
# 初始化左右两侧遇到的最大高度
left_max, right_max = 0, 0
# 初始化总接水量
water = 0
# 当左指针在右指针左侧时,继续循环
while left < right:
# 更新左侧遇到的最大高度
left_max = max(left_max, height[left])
# 更新右侧遇到的最大高度
right_max = max(right_max, height[right])
# 核心逻辑:哪边的最大高度小,就先处理哪边
# 因为接水量取决于较短的那块木板(短板效应)
if left_max < right_max:
# 左边较短,说明 left 位置的水位由 left_max 决定
# 因为右边至少有一个 right_max 比 left_max 高,水不会漏出去
water += left_max - height[left]
left += 1 # 左指针向右移动
else:
# 右边较短,说明 right 位置的水位由 right_max 决定
# 因为左边至少有一个 left_max 比 right_max 高
water += right_max - height[right]
right -= 1 # 右指针向左移动
return water
def trap_dp(self, height: List[int]) -> int:
"""
解法二:动态规划 (Dynamic Programming)
时间复杂度:O(n)
空间复杂度:O(n)
思路:预先计算每个位置左边和右边的最大高度,存储在数组中。
"""
if not height or len(height) < 3:
return 0
n = len(height)
# left_max[i] 表示位置 i 左边(包括 i)的最高柱子高度
left_max = [0] * n
# right_max[i] 表示位置 i 右边(包括 i)的最高柱子高度
right_max = [0] * n
# 1. 从左向右遍历,填充 left_max 数组
left_max[0] = height[0]
for i in range(1, n):
left_max[i] = max(left_max[i-1], height[i])
# 2. 从右向左遍历,填充 right_max 数组
right_max[n-1] = height[n-1]
for i in range(n-2, -1, -1):
right_max[i] = max(right_max[i+1], height[i])
# 3. 遍历每个位置,计算接水量并累加
water = 0
for i in range(n):
# 当前位置水位 = min(左边最高,右边最高) - 当前高度
water += min(left_max[i], right_max[i]) - height[i]
return water
def trap_stack(self, height: List[int]) -> int:
"""
解法三:单调栈 (Monotonic Stack)
时间复杂度:O(n)
空间复杂度:O(n)
思路:按层计算雨水。维护一个递减栈,遇到比栈顶高的柱子时,说明形成了凹槽。
"""
if not height or len(height) < 3:
return 0
stack = [] # 栈中存储柱子的索引,保持对应的高度单调递减
water = 0
for i, h in enumerate(height):
# 当当前柱子高度大于栈顶柱子高度时,说明可能形成凹槽
while stack and h > height[stack[-1]]:
# 弹出栈顶元素,作为凹槽的底部
bottom = stack.pop()
# 如果栈为空,说明没有左边界,无法形成凹槽,跳出循环
if not stack:
break
# 新的栈顶元素作为左边界
left = stack[-1]
# 计算凹槽的宽度:当前索引 - 左边界索引 - 1
width = i - left - 1
# 计算凹槽的高度:min(左边界高度,右边界高度) - 底部高度
bounded_height = min(height[left], h) - height[bottom]
# 累加这一层的雨水量
water += width * bounded_height
# 将当前柱子索引入栈
stack.append(i)
return water
# --- 测试代码 ---
if __name__ == "__main__":
sol = Solution()
# 测试用例
test_height = [0,1,0,2,1,0,1,3,2,1,2,1]
print(f"输入高度图:{test_height}")
print(f"双指针法结果:{sol.trap(test_height)}") # 期望输出:6
print(f"动态规划法结果:{sol.trap_dp(test_height)}") # 期望输出:6
print(f"单调栈法结果:{sol.trap_stack(test_height)}") # 期望输出:6
4. 算法复杂度对比总结
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 | 推荐度 |
|---|---|---|---|---|
| 动态规划 | O(n) | O(n) | 逻辑简单,容易编写,适合初学者理解原理 | ⭐⭐⭐⭐ |
| 双指针 | O(n) | O(1) | 空间最优,面试标准答案,性能最好 | ⭐⭐⭐⭐⭐ |
| 单调栈 | O(n) | O(n) | 适合处理横向凹槽问题,思路独特 | ⭐⭐⭐ |
5. 常见疑问解答
Q1: 双指针法中,为什么 left_max < right_max 时可以直接计算 left 处的水量?
- 答 :因为
right_max代表的是right指针及其右侧遇到的最大值。既然left_max < right_max,说明在left的右侧一定存在一个高度至少为right_max的柱子。对于left位置来说,它左边的最高是left_max,右边的最高至少是right_max。根据木桶效应,水位高度取决于较小值,也就是left_max。所以此时left处的水量是确定的,可以放心计算并移动指针。
Q2: 单调栈中 width = i - left - 1 是怎么来的?
- 答 :
i是当前右边界索引,left是弹出底部后新的栈顶(左边界)索引。凹槽的宽度是左右边界之间的距离,不包含边界本身,所以是i - left - 1。例如左边界在索引 1,右边界在索引 4,中间能接水的柱子索引是 2 和 3,宽度为 2,即4 - 1 - 1 = 2。
Q3: 为什么数组长度小于 3 时直接返回 0?
- 答:接雨水至少需要三根柱子才能形成一个凹槽(左边界、底部、右边界)。如果只有 1 根或 2 根柱子,水会直接流走,无法储存。