找回密码
 立即注册
首页 业界区 业界 LeetCode 42 接雨水:python3 题解

LeetCode 42 接雨水:python3 题解

碛物 5 小时前
目录

  • 1. 问题核心理解
  • 2. 解法详解

    • 方法一:动态规划(预处理数组)
    • 方法二:双指针法(最优解)⭐
    • 方法三:单调栈(按层计算)

  • 3. 完整代码实现(Python 3)
  • 4. 算法复杂度对比总结
  • 5. 常见疑问解答

1. 问题核心理解

题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
直观理解
想象一下,雨水会落在柱子之间的凹陷处。对于任意一个位置 i,它能存多少水,取决于它左边最高的柱子右边最高的柱子

  • 水往低处流,所以水位高度不能超过左右两侧最高柱子中较矮的那个(木桶效应)。
  • 当前位置能接的水量 = min(左边最高,右边最高) - 当前位置高度。
  • 如果计算结果为负数,说明当前位置比边界高,接不到水,记为 0。
核心公式

\[\text{water} = \max(0, \min(\text{left_max}, \text{right_max}) - \text{height})\]
下面我将详细讲解三种主流解法:动态规划双指针(最优解)、单调栈
2. 解法详解

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

思路
核心公式需要知道每个位置左右的最大值。暴力法是每次遍历都重新扫描左右,效率低。动态规划的核心思想是空间换时间

  • 创建两个数组 left_max 和 right_max。
  • left_max 存储位置 i 及其左边的最高柱子高度。
  • right_max 存储位置 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(单调栈)方法。
  1. from typing import List
  2. class Solution:
  3.     def trap(self, height: List[int]) -> int:
  4.         """
  5.         主解法:双指针法 (Two Pointers)
  6.         时间复杂度:O(n)
  7.         空间复杂度:O(1)
  8.         这是面试中最推荐的解法,因为它在时间和空间上都是最优的。
  9.         """
  10.         # 边界条件:如果柱子数量小于 3,无法形成凹槽接水
  11.         if not height or len(height) < 3:
  12.             return 0
  13.         
  14.         # 初始化左右指针,分别指向数组的头和尾
  15.         left, right = 0, len(height) - 1
  16.         
  17.         # 初始化左右两侧遇到的最大高度
  18.         left_max, right_max = 0, 0
  19.         
  20.         # 初始化总接水量
  21.         water = 0
  22.         
  23.         # 当左指针在右指针左侧时,继续循环
  24.         while left < right:
  25.             # 更新左侧遇到的最大高度
  26.             left_max = max(left_max, height[left])
  27.             # 更新右侧遇到的最大高度
  28.             right_max = max(right_max, height[right])
  29.             
  30.             # 核心逻辑:哪边的最大高度小,就先处理哪边
  31.             # 因为接水量取决于较短的那块木板(短板效应)
  32.             if left_max < right_max:
  33.                 # 左边较短,说明 left 位置的水位由 left_max 决定
  34.                 # 因为右边至少有一个 right_max 比 left_max 高,水不会漏出去
  35.                 water += left_max - height[left]
  36.                 left += 1  # 左指针向右移动
  37.             else:
  38.                 # 右边较短,说明 right 位置的水位由 right_max 决定
  39.                 # 因为左边至少有一个 left_max 比 right_max 高
  40.                 water += right_max - height[right]
  41.                 right -= 1  # 右指针向左移动
  42.                
  43.         return water
  44.     def trap_dp(self, height: List[int]) -> int:
  45.         """
  46.         解法二:动态规划 (Dynamic Programming)
  47.         时间复杂度:O(n)
  48.         空间复杂度:O(n)
  49.         思路:预先计算每个位置左边和右边的最大高度,存储在数组中。
  50.         """
  51.         if not height or len(height) < 3:
  52.             return 0
  53.         
  54.         n = len(height)
  55.         # left_max[i] 表示位置 i 左边(包括 i)的最高柱子高度
  56.         left_max = [0] * n
  57.         # right_max[i] 表示位置 i 右边(包括 i)的最高柱子高度
  58.         right_max = [0] * n
  59.         
  60.         # 1. 从左向右遍历,填充 left_max 数组
  61.         left_max[0] = height[0]
  62.         for i in range(1, n):
  63.             left_max[i] = max(left_max[i-1], height[i])
  64.             
  65.         # 2. 从右向左遍历,填充 right_max 数组
  66.         right_max[n-1] = height[n-1]
  67.         for i in range(n-2, -1, -1):
  68.             right_max[i] = max(right_max[i+1], height[i])
  69.             
  70.         # 3. 遍历每个位置,计算接水量并累加
  71.         water = 0
  72.         for i in range(n):
  73.             # 当前位置水位 = min(左边最高,右边最高) - 当前高度
  74.             water += min(left_max[i], right_max[i]) - height[i]
  75.             
  76.         return water
  77.     def trap_stack(self, height: List[int]) -> int:
  78.         """
  79.         解法三:单调栈 (Monotonic Stack)
  80.         时间复杂度:O(n)
  81.         空间复杂度:O(n)
  82.         思路:按层计算雨水。维护一个递减栈,遇到比栈顶高的柱子时,说明形成了凹槽。
  83.         """
  84.         if not height or len(height) < 3:
  85.             return 0
  86.         
  87.         stack = []  # 栈中存储柱子的索引,保持对应的高度单调递减
  88.         water = 0
  89.         
  90.         for i, h in enumerate(height):
  91.             # 当当前柱子高度大于栈顶柱子高度时,说明可能形成凹槽
  92.             while stack and h > height[stack[-1]]:
  93.                 # 弹出栈顶元素,作为凹槽的底部
  94.                 bottom = stack.pop()
  95.                
  96.                 # 如果栈为空,说明没有左边界,无法形成凹槽,跳出循环
  97.                 if not stack:
  98.                     break
  99.                
  100.                 # 新的栈顶元素作为左边界
  101.                 left = stack[-1]
  102.                
  103.                 # 计算凹槽的宽度:当前索引 - 左边界索引 - 1
  104.                 width = i - left - 1
  105.                
  106.                 # 计算凹槽的高度:min(左边界高度,右边界高度) - 底部高度
  107.                 bounded_height = min(height[left], h) - height[bottom]
  108.                
  109.                 # 累加这一层的雨水量
  110.                 water += width * bounded_height
  111.             
  112.             # 将当前柱子索引入栈
  113.             stack.append(i)
  114.             
  115.         return water
  116. # --- 测试代码 ---
  117. if __name__ == "__main__":
  118.     sol = Solution()
  119.    
  120.     # 测试用例
  121.     test_height = [0,1,0,2,1,0,1,3,2,1,2,1]
  122.     print(f"输入高度图:{test_height}")
  123.     print(f"双指针法结果:{sol.trap(test_height)}")       # 期望输出:6
  124.     print(f"动态规划法结果:{sol.trap_dp(test_height)}")   # 期望输出:6
  125.     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 根柱子,水会直接流走,无法储存。



来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册