【小白笔记】接雨水(双指针)

《接雨水》是面试中极具分量的题目。它的核心难点在于:如何确定某一个位置能接多少水?
只要搞清楚了“局部”的逻辑,整体的算法就呼之欲出了。
1. 核心物理原理:木桶效应
对于位置 i 的柱子,它能接多少水,取决于它左边最高的柱子和右边最高的柱子的“较短者”。
2. 解法一:双指针(面试最优解,空间 )
与其提前算出所有位置的左右最大值,不如用两个指针从两端向中间移动,边走边更新。
- 原理:如果左边的墙比右边的墙矮,那么左边这个位置的水量就只取决于“左边最高的墙”,因为右边一定有更高的墙能兜住水。
代码实现 (Python)
class Solution:
def trap(self, height: list[int]) -> int:
if not height: return 0
left, right = 0, len(height) - 1
left_max, right_max = 0, 0
res = 0
while left < right:
# 更新左右两边的最高纪录
left_max = max(left_max, height[left])
right_max = max(right_max, height[right])
# 哪边矮,就结算哪边
if left_max < right_max:
res += left_max - height[left]
left += 1
else:
res += right_max - height[right]
right -= 1
return res
3. 解法二:动态规划(最容易理解,空间 )
如果你觉得双指针太玄学,可以先记这个逻辑:
- 左向右扫一遍:记录每个位置
i左边出现过的最大高度left_max[i]。 - 右向左扫一遍:记录每个位置
i右边出现过的最大高度right_max[i]。 - 最后扫一遍:计算每个点的积水并累加。
4. ACM 模式下的写法
这道题在 ACM 模式中通常给出一串空格分隔的数字。我们可以复用之前的万能模板:
import sys
def solve():
# 1. 读取输入
# sys.stdin.read().split() 会把所有数字读进列表
data = sys.stdin.read().split()
if not data: return
# 2. 转化为整数列表
# 有的题目第一行是数量,如果是,则取 data[1:]
height = [int(x) for x in data]
# 3. 核心算法逻辑
left, right = 0, len(height) - 1
l_max, r_max = 0, 0
ans = 0
while left < right:
l_max = max(l_max, height[left])
r_max = max(r_max, height[right])
if l_max < r_max:
ans += l_max - height[left]
left += 1
else:
ans += r_max - height[right]
right -= 1
# 4. 输出
print(ans)
if __name__ == "__main__":
solve()
💡 面试官的连环炮:
- 问: “这题能用单调栈吗?”
- 答: “可以。单调递减栈可以找‘低洼处’。每当我们遇到一个比栈顶高的柱子,就形成了一个坑,可以计算横向的水量。”
- 问: “双指针法为什么不需要担心中间有更高的柱子?”
- 答: “因为只要
left_max < right_max,即使中间有更高的,左边的积水也只会被left_max限制。我们是从边缘逐步推进的,保证了逻辑的严密性。”
双指针(Two Pointers)和滑动窗口(Sliding Window)就像是**“快餐店的夹子”和“传送带上的盒子”**。它们看似相似,但核心逻辑和解决的问题完全不同。
1. 双指针的思想、做法与题型
双指针是一种通过**两个变量(指针)**在序列上同时移动,从而将 的暴力搜索降低到 或 的优化思想。
常见做法分类:
- 对撞指针(左右指针):
- 做法:一个在头
left,一个在尾right,向中间靠拢。 - 典型题型:二分查找、反转数组/字符串、回文串判断、两数之和(有序数组)、接雨水。
- 口诀:两头堵,找中间。
- 快慢指针(同步同向):
- 做法:两个指针都从起点开始,但移动步长不同(如快指针走两步,慢指针走一步)。
- 典型题型:链表是否有环、寻找链表中点、原地修改数组(删除重复项)。
- 口诀:一个追,一个跑。
- 分离指针(两个数组):
- 做法:两个指针分别在两个不同的数组/链表上移动。
- 典型题型:合并两个有序数组/链表、判断子序列。
2. 双指针 vs 滑动窗口:怎么区分?
虽然滑动窗口也是用两个指针(left 和 right),但它们的侧重点完全不同。
双指针 (Two Pointers)
- 侧重于“点”:通常关注的是这两个指针指向的两个特定元素。
- 动作:寻找满足某种关系的一对元素。
- 例子(接雨水/两数之和):我关心的是
left指向的高度和right指向的高度,以此来决定下一步操作。
滑动窗口 (Sliding Window)
- 侧重于“面”:关注的是两个指针之间的整个区间(子数组)。
- 动作:维护一个窗口,随着窗口滑动,动态地更新窗口内所有元素的总和、个数、状态等。
- 例子(最长无重复子串/最小覆盖子串):我不只看
left和right指向的字符,我关心的是left到right这一坨字符里有没有重复的。
3. 对比总结表
| 维度 | 双指针 (常规) | 滑动窗口 |
|---|---|---|
| 关注对象 | 两个孤立的元素 | 一个连续的子区间 |
| 指针移动 | 灵活(对撞、快慢、跳跃) | 相对固定(右扩、左缩) |
| 判断条件 | 这两个数加起来等于 target 吗? | 这个区间里的数字和超过 k 了吗? |
| 返回结果 | 返回两个下标或特定值 | 返回区间的长度、内容或最大/最小值 |
💡 实战判断准则
- 如果题目提到**“子数组”、“连续子序列”、“长度为 k 的窗口”** 滑动窗口。
- 如果题目提到**“有序数组”、“两个数的和”、“原地修改”、“判断回文”** 双指针。
4. 举个例子说明区别
-
题目 A(双指针):在一个有序数组中,找两个数,使它们的和等于 10。
-
动作:
left在头,right在尾,相加太大就right--。我们只看这两个数。 -
题目 B(滑动窗口):在一个正整数数组中,找一个最短的连续子数组,使它的和大于等于 10。
-
动作:
right不断向右加数,和够了之后,left开始向右缩,记录缩的过程中最短的长度。我们看的是[left, right]这一整段。
指针在代码实现上确实就是一个 index(整数索引),它只是逻辑上的一个“箭头”。
《接雨水》的双指针解法,对应的是对撞指针(Collision Pointers)。
我们可以从以下三个维度来深度拆解它的本质:
1. 为什么是“对撞指针”?
对撞指针的特征是:一左一右,向中间靠拢,直到相遇。
- 起点:
left = 0,right = n - 1。 - 移动规律:每一轮循环中,根据左右两边“墙高度”的比较结果,决定是
left += 1还是right -= 1。 - 终点:
while left < right,当两个索引(指针)重合时,说明整个数组都被扫描完毕,所有位置的水量都结算完了。
2. 这个“对撞”的逻辑本质是什么?
在《接雨水》中,对撞指针实际上是在模拟一个**“缩减边界”**的过程。
- 常规对撞指针(如两数之和):是根据
sum的大小来决定动哪边。 - 接雨水对撞指针:是根据 “哪边的短板更短” 来决定动哪边。
本质逻辑推导:
- 水量的瓶颈在于“较低的那一侧”。
- 如果我们知道左侧的最高墙
left_max比右侧的最高墙right_max矮,那么对于left指针指向的位置,它的水位上限一定是被left_max锁死的(因为右边已经存在一个比它更高的right_max了)。 - 既然水位已经确定了,我们就结算
left的水,然后让left向中间挪一步,寻找下一个结算点。
3. 指针类型大对比(帮你彻底分清)
既然你提到了这几种指针,我们用一句话总结它们的区别,方便你记忆:
| 指针类型 | 动作描述 | 典型题型 |
|---|---|---|
| 对撞指针 | 一头一尾,往中间挤 | 接雨水、两数之和、翻转数组、回文判断 |
| 快慢指针 | 同向出发,一个快一个慢 | 链表找中点、判断环、删除重复元素 |
| 分离指针 | 两个数组,各走各的 | 合并两个有序数组、判断子序列 |
| 滑动窗口 | 一前一后,维护一个区间 | 无重复最长子串、和大于K的最短子数组 |
💡 深度思考:指针即索引
你说“指针也是一个index”,这抓住了编程的本质。在 Python/Java/C++ 数组题目中:
- 空间上:它就是一个
int变量。 - 逻辑上:它代表了我们**“当前关注的状态位”**。
在《接雨水》里,这两个 index 就是两个探测器。它们从两头出发,互相给对方“兜底”。right 指针在那站着,其实是给 left 指针一个承诺:“我这边有高墙,你那边只要比我矮,你就放心大胆地按你自己的左侧高墙算水吧!”
面试小贴士:
如果面试官问你:“为什么这道题可以用对撞指针优化到 空间?”
你可以回答:“因为接雨水满足单调性约束。每一列的水位只取决于两边最矮的那一侧,通过对撞指针,我们始终在处理较小的那一侧,从而保证了在不需要存储所有 left_max 和 right_max 的情况下,依然能正确结算每一列的水量。”
更多推荐
所有评论(0)