在这里插入图片描述
《接雨水》是面试中极具分量的题目。它的核心难点在于:如何确定某一个位置能接多少水?

只要搞清楚了“局部”的逻辑,整体的算法就呼之欲出了。


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. 解法二:动态规划(最容易理解,空间 )

如果你觉得双指针太玄学,可以先记这个逻辑:

  1. 左向右扫一遍:记录每个位置 i 左边出现过的最大高度 left_max[i]
  2. 右向左扫一遍:记录每个位置 i 右边出现过的最大高度 right_max[i]
  3. 最后扫一遍:计算每个点的积水并累加。

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()


💡 面试官的连环炮:

  1. 问: “这题能用单调栈吗?”
  • 答: “可以。单调递减栈可以找‘低洼处’。每当我们遇到一个比栈顶高的柱子,就形成了一个坑,可以计算横向的水量。”
  1. 问: “双指针法为什么不需要担心中间有更高的柱子?”
  • 答: “因为只要 left_max < right_max,即使中间有更高的,左边的积水也只会被 left_max 限制。我们是从边缘逐步推进的,保证了逻辑的严密性。”

双指针(Two Pointers)和滑动窗口(Sliding Window)就像是**“快餐店的夹子”“传送带上的盒子”**。它们看似相似,但核心逻辑和解决的问题完全不同。


1. 双指针的思想、做法与题型

双指针是一种通过**两个变量(指针)**在序列上同时移动,从而将 的暴力搜索降低到 或 的优化思想。

常见做法分类:
  1. 对撞指针(左右指针)
  • 做法:一个在头 left,一个在尾 right,向中间靠拢。
  • 典型题型二分查找反转数组/字符串回文串判断两数之和(有序数组)接雨水
  • 口诀:两头堵,找中间。
  1. 快慢指针(同步同向)
  • 做法:两个指针都从起点开始,但移动步长不同(如快指针走两步,慢指针走一步)。
  • 典型题型链表是否有环寻找链表中点原地修改数组(删除重复项)
  • 口诀:一个追,一个跑。
  1. 分离指针(两个数组)
  • 做法:两个指针分别在两个不同的数组/链表上移动。
  • 典型题型合并两个有序数组/链表判断子序列

2. 双指针 vs 滑动窗口:怎么区分?

虽然滑动窗口也是用两个指针(leftright),但它们的侧重点完全不同。

双指针 (Two Pointers)
  • 侧重于“点”:通常关注的是这两个指针指向的两个特定元素
  • 动作:寻找满足某种关系的一对元素
  • 例子(接雨水/两数之和):我关心的是 left 指向的高度和 right 指向的高度,以此来决定下一步操作。
滑动窗口 (Sliding Window)
  • 侧重于“面”:关注的是两个指针之间的整个区间(子数组)
  • 动作:维护一个窗口,随着窗口滑动,动态地更新窗口内所有元素的总和、个数、状态等。
  • 例子(最长无重复子串/最小覆盖子串):我不只看 leftright 指向的字符,我关心的是 leftright 这一坨字符里有没有重复的。

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 的大小来决定动哪边。
  • 接雨水对撞指针:是根据 “哪边的短板更短” 来决定动哪边。

本质逻辑推导:

  1. 水量的瓶颈在于“较低的那一侧”。
  2. 如果我们知道左侧的最高墙 left_max 比右侧的最高墙 right_max 矮,那么对于 left 指针指向的位置,它的水位上限一定是被 left_max 锁死的(因为右边已经存在一个比它更高的 right_max 了)。
  3. 既然水位已经确定了,我们就结算 left 的水,然后让 left 向中间挪一步,寻找下一个结算点。

3. 指针类型大对比(帮你彻底分清)

既然你提到了这几种指针,我们用一句话总结它们的区别,方便你记忆:

指针类型动作描述典型题型
对撞指针一头一尾,往中间挤接雨水、两数之和、翻转数组、回文判断
快慢指针同向出发,一个快一个慢链表找中点、判断环、删除重复元素
分离指针两个数组,各走各的合并两个有序数组、判断子序列
滑动窗口一前一后,维护一个区间无重复最长子串、和大于K的最短子数组

💡 深度思考:指针即索引

你说“指针也是一个index”,这抓住了编程的本质。在 Python/Java/C++ 数组题目中:

  • 空间上:它就是一个 int 变量。
  • 逻辑上:它代表了我们**“当前关注的状态位”**。

在《接雨水》里,这两个 index 就是两个探测器。它们从两头出发,互相给对方“兜底”。right 指针在那站着,其实是给 left 指针一个承诺:“我这边有高墙,你那边只要比我矮,你就放心大胆地按你自己的左侧高墙算水吧!”

面试小贴士:
如果面试官问你:“为什么这道题可以用对撞指针优化到 空间?”
你可以回答:“因为接雨水满足单调性约束。每一列的水位只取决于两边最矮的那一侧,通过对撞指针,我们始终在处理较小的那一侧,从而保证了在不需要存储所有 left_maxright_max 的情况下,依然能正确结算每一列的水量。”

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐