
如果有人让我给准备算法面试的朋友只推荐一道题力扣第11题“盛最多水的容器”一定会出现在我的Top 3名单里。这题在“力扣热题100”中长期霸榜题目描述短到扫一眼就能看懂最优解法的核心代码不超过十行可它背后藏着的“双指针压缩搜索空间”思想是后面一堆进阶题的地基。很多人把这道题当作双指针的入门模板也有不少人面试时栽在它的变体上。这篇刷题笔记我打算把从暴力解到最优解、从代码到证明、从踩坑到面试发挥的完整链路都梳理一遍适合刚刷力扣的新手也适合想把这题彻底吃透、再去碰“接雨水”这类进阶题的同学。1. 题目解读与核心考点拆解1.1 这道题到底在问什么题目给了一个长度为n的整数数组height每个下标i对应一条垂直于 x 轴的线线的两个端点是(i, height[i])和(i, 0)。我们要从这些线里挑两条和 x 轴一起构成一个容器问最多能装多少水。装水量怎么算两条线之间的距离是容器的“宽”也就是下标的差值j - i容器的高度由两条线中较短的那条决定也就是min(height[i], height[j])。所以面积公式就是[ S min(height[i], height[j]) \times (j - i) ]题目的输入要求n 2因为至少要有两条线才能构成容器。最终要返回这个最大面积而不是返回是哪两条线。我用一个生活化的比喻来理解一排高低不等的木板竖在河边你随手拿两块板当左右墙往中间倒水。水会不会漫出来取决于矮的那块板。你要做的就是在这排木板里挑两块让中间蓄水的体积最大。注意木板本身不能倾斜水也不会从高的那块板那一侧漫出去所以矮板是天花板。这道题有一个容易忽略的细节面积只跟两个端点有关跟中间的木板完全无关。也就是说(i, j)这个容器中间哪怕有一根通天高的柱子也不影响水量。理解这一点后面理解双指针排除区间时才不会卡壳。1.2 为什么这道题值得反复刷第11题之所以在“力扣热题100”里地位这么高不是因为它难而是因为它把算法学习中几个重要的东西浓缩在了一道简单题里。第一它是“暴力枚举 → 优化 → 最优解”的典型范例。暴力做法是枚举所有(i, j)组合复杂度O(n^2)。双指针做法是O(n)一次扫描。从O(n^2)到O(n)中间那个“为什么可以跳过大量组合”的思考过程比代码本身值钱得多。第二它体现了“缩小搜索空间”的核心思想。双指针为什么能从两端往中间收因为每一步我们都能数学上证明某一条边不可能是最优解的一部分所以可以大胆排除。这种“排除法”逻辑和接雨水、三数之和、有序数组的两数之和都是同源的。你把第11题吃透了再看那些题会感觉似曾相识。第三它适合面试官层层追问。先让你说思路再问“为什么移动矮的那一侧而不是高的那一侧”再问“如果两个高度相等怎么办”再让你证明算法的正确性。能把这一串问题答清楚的人说明不是背模板而是真的理解了搜索空间的压缩过程。很多刷题资料比如 labuladong 的算法笔记里也会把这类题归到“双指针技巧”专题下作为开篇例题。我的建议是不要只看题解就划走把证明自己写一遍把示例手动推演一遍这道题才算真正刷完。1.3 适合谁来刷刷到什么程度算过关这道题的受众很广不同阶段的人应该给自己设定不同的目标线。刚接触算法的同学目标可以是能写出暴力解能看懂双指针解能说出“移动短板”这个直觉。这个阶段先不追求完美证明关键是建立“面积由短板决定”的思维模型。刷题一段时间、准备面试的同学目标要高一点能独立写出双指针代码能徒手证明正确性能主动聊边界情况比如数组只有两根线、两根线高度相等、存在大数溢出风险等。面试官顺着题目追问时你要能接得住。已经工作、在刷第二轮第三轮的人可以往“模型迁移”方向走把这道题和接雨水、最大矩形、三数之和放在一起对比提炼出“什么时候该用双指针收窄”、“什么时候该用单调栈”。到这个程度一道题就不只是一道题了而是一类题的入口。我个人判断“过关”的标准很简单关掉题解十分钟内写出无 bug 的代码并且能用自己的话讲清楚“为什么移动矮板不会漏掉最优解”。如果做到这两点这道题就算真正刷透了。好的题目解读和考点拆解就到这里。接下来进入核心部分思路是怎么从暴力一步步走到双指针的以及那个关键的“正确性证明”到底怎么理解。2. 思路演进从暴力枚举到双指针收缩2.1 暴力法先保证能跑通拿到一道题最稳妥的第一步永远是先确认自己理解题意而暴力法就是用来确认理解的。对这道题来说暴力法非常直观遍历所有下标对(i, j)其中j i计算每个容器的面积记录最大值。代码长这样def maxArea(height): n len(height) ans 0 for i in range(n): for j in range(i 1, n): area min(height[i], height[j]) * (j - i) ans max(ans, area) return ans两层循环每个组合都算一遍时间复杂度是O(n^2)。空间复杂度是O(1)因为没有额外开数组。这个解法能通过示例但在力扣的测试数据下n最多可以到10^5O(n^2)意味着最多10^10次操作跑完基本要等天荒地老。所以暴力法只能作为思路验证不能作为最终答案。不过我有句话要送给你面试时如果发现题目没完全看懂先别慌你完全可以说“我先用暴力方法确认一下题意”。这不是丢人的事反而说明你有结构化思维。真正的问题是很多人写完了暴力解就说“优化不会了”这就有点浪费。从暴力到最优解关键不是技巧的堆砌而是想清楚一个问题我们到底能不能少算一些组合哪些组合是注定不可能成为答案的2.2 核心直觉短板决定上限先盯着面积公式看[ S min(height[i], height[j]) \times (j - i) ]假设现在指针i指向数组最左边指针j指向数组最右边也就是宽度最大的情况。这时如果我们要让指针往中间移动宽度j - i一定在变小。这是个非常重要的观察无论你移动哪边的指针宽度都在变小。所以想让面积变大唯一的指望就是“变矮的那块板”变高。如果移动的是高板那一侧新板即使比原来的高板还高对面积也毫无帮助因为容器高度被短板锁死了如果新板比原来的高板矮那面积只会更小。也就是说移动高板结果只可能是“不变小或者变小”绝不可能变大。举个例子假设height[i] 5height[j] 10当前宽度是10当前面积是5 * 10 50。如果移动高板j新位置的高度假设是12那容器高度还是min(5, 12) 5宽度却变成了9面积变成45变小了。新位置高度假设是3那容器高度是3面积是27更小了。无论哪种情况移动高板都没好果子吃。那移动矮板呢移动矮板后新板的高度有可能会比原来高这样容器的“短板”可能被抬高面积就有机会变大。即便新板更矮结果变差了我们也没有损失因为最差的结果只不过是把这组候选排除掉答案不会比当前更差。于是双指针的策略就呼之欲出了每次计算当前两个指针构成的容器面积更新答案然后把较矮的那一侧往中间移动一步重复这个过程直到两个指针相遇。这就是这道题的核心直觉。它本质上是一个贪心选择每一步只看当前局面选择“唯一有可能让结果变好的方向”。2.3 正确性证明排除掉的区域为什么安全看到这里很多人会有一个疑问每一步都基于当前的两条线做判断万一最优解需要的不是当前这个矮板而是某个“当前看起来没用”的高板呢移动矮板会不会把最优解给跳过去这是个好问题也是面试官最爱追问的点。要回答它需要做一个数学上严格的证明。我们假设某一步的指针状态是i和j并且height[i] height[j]。当前容器面积是[ S_{ij} height[i] \times (j - i) ]现在我要证明以i作为左边界、右边界在(i, j)之间的所有容器面积都不可能超过S_{ij}。为什么取任意一个右边界k满足i k j。这个容器的面积是[ S_{ik} min(height[i], height[k]) \times (k - i) ]它的宽度k - i严格小于j - i。再说高度min(height[i], height[k])最多就是height[i]不可能超过它。所以[ S_{ik} \le height[i] \times (k - i) height[i] \times (j - i) S_{ij} ]也就是说只要右边界在i和j之间不管中间那根板有多高构成的容器面积都严格小于当前面积。结论是以i为左边界的容器最优情况就是当前这个(i, j)而它已经被我们算过了。所以左指针i可以安全地向右移动我们不会漏掉任何可能的更大值。反过来如果height[i] height[j]也就是右指针是短板或两边一样高用同样的逻辑可以证明以j为右边界的任何容器面积都不可能超过当前的S_{ij}。所以右指针j可以安全地向左移动。这个证明的妙处在于它不是“感觉上应该没错”而是每一步都严格排除了一个边界上所有可能的候选答案。整个过程下来我们相当于把二维的搜索空间逐步裁剪最终剩下的就是最优解。有人可能会问如果两个高度相等呢比如height[i] height[j]移动哪边我们看代码里的条件if height[i] height[j]移动左指针否则移动右指针。所以相等时移动右指针。但这只是习惯你写成if height[i] height[j]移动左指针也是对的。原因是当两边相等时无论移动哪一边被排除的那个边界上的所有方案也都被证明不会更优了证明过程和上面完全一样。所以相等时移动哪个都安全只要别两个都不动就行。这一段证明是整道题的精髓。我的建议是不要光看拿起笔在纸上自己写一遍然后用一个具体数组把每一步都验证一遍。你亲手推过一次之后这道题就很难忘了。3. 代码实现与细节打磨3.1 Python 主方案逐行拆解先给出最常用的 Python 写法这也是我刷力扣时最顺手的一个版本def maxArea(height): i, j 0, len(height) - 1 ans 0 while i j: area min(height[i], height[j]) * (j - i) ans max(ans, area) if height[i] height[j]: i 1 else: j - 1 return ans这段代码只有八行但每一行都值得细说。初始化i 0和j len(height) - 1让两个指针一开始就处于最大宽度的位置。为什么从最宽开始因为宽度是面积的一部分从最大宽度出发配合“排除不可能边界”的证明可以保证覆盖所有最优解候选。ans初始化为0是安全的因为题目给出的height都是非负整数任意两个不同下标之间的面积至少为1 * 1 1前提是高度不为 0所以0作为初始最小值不会干扰结果。循环条件while i j保证两个指针不重叠。如果i j那就是同一条线构成不了容器没有意义。这个条件一定要写好写反了或者漏了都有可能死循环或者漏解。循环体里第一步就是计算当前面积min(height[i], height[j])取短板(j - i)是宽度。这里最容易犯的错误是把min写成max我当年第一次写这题就栽在这上面。你想想容器能装多少水是由矮板决定的要是取高的那根那计算出来的“面积”根本不可能装那么多水整个思路就歪了。更新答案用max(ans, area)这个没什么好说的。重点在后面的指针移动逻辑if height[i] height[j]: i 1 else: j - 1。这个条件跟我们在 2.3 节证明的结论一一对应矮的那侧往中间移动。注意这里比较的是两条线的高度不是面积也不是宽度。很多人写着写着就写成了if area ans之类的东西完全是两码事。整个循环最多执行n - 1次因为每次移动一个指针两个指针相遇时循环结束。这个遍历次数是严格线性的不存在中间反复横跳的情况。3.2 主流语言的写法对照同样的思路换到 Java、C、Go 等语言里逻辑几乎不用变只是语法换了一下。我整理了几个常见版本面试时你手上是什么语言直接照着写就行。Java 版本class Solution { public int maxArea(int[] height) { int i 0, j height.length - 1; int ans 0; while (i j) { int area Math.min(height[i], height[j]) * (j - i); ans Math.max(ans, area); if (height[i] height[j]) { i; } else { j--; } } return ans; } }C 版本class Solution { public: int maxArea(vectorint height) { int i 0, j height.size() - 1; int ans 0; while (i j) { int area min(height[i], height[j]) * (j - i); ans max(ans, area); if (height[i] height[j]) i; else j--; } return ans; } };Go 版本func maxArea(height []int) int { i, j : 0, len(height) - 1 ans : 0 for i j { area : min(height[i], height[j]) * (j - i) if area ans { ans area } if height[i] height[j] { i } else { j-- } } return ans } func min(a, b int) int { if a b { return a } return b }几个语言的差异点Java 的Math.min和Math.max对int直接生效注意height是int[]不需要拆箱C 的min和max是标准库函数注意vectorint的size()返回size_t严格来说和int混用会有类型转换不过这道题的长度上限在int范围内问题不大Go 语言没有内置的min/max泛型函数老版本需要自己写一个或者用sort.Ints之类的笨办法但为了性能还是手写个小函数最干净。不管用哪种语言核心循环条件、指针移动规则都不能变。换语言只是换一套表达方式算法思想是同一套。3.3 边界条件与防御性编程刷题和写业务代码一样不能只盯着“正常情况”边界条件往往是隐藏的扣分点。第一个边界是数组长度。题目说n 2但你在写的时候最好还是判断一下如果height为空或者长度为 1直接返回 0。这样做不是因为题目会给你非法输入而是为了养成防御性编程的习惯。面试官如果看到你主动写这个判断会认为你对空输入有意识。第二个边界是高度为 0 的线。如果数组里有一根高度为 0 的线它跟任何线组成的容器面积都是 0因为min(0, x) 0。这不影响算法正确性但你在手推例子时要注意别被 0 迷惑。第三个边界是大数溢出。题目给出的height元素范围是[0, 10^4]n最大是10^5所以最大面积大概是10^4 * 10^5 10^9还在int范围内。但如果你是做拓展题或者面试官突然说“如果数据范围变成10^9呢”你就要想到用long来存面积。在 Python 里不存在这个问题因为整数可以无限大但在 Java、C 里这是一道经典的对“数值范围敏感度”的考察。第四个边界是两指针相遇时怎么处理。当i j时循环停止此时不应该再计算面积因为同一条线不能构成容器。有些变体题会要求你处理i和j重合的情况但本题不需要。第五个隐藏的边界是“所有高度都一样”。比如[3, 3, 3, 3]双指针每次会移动右指针因为height[i] height[j]时走 else 分支最后答案是3 * 3 9也就是最左边和最右边构成的容器。这个例子可以用来验证你代码里的“相等高度走哪个分支”是否符合预期。边界情况总结一下其实就一句话代码不仅要在大样本上跑得快还要在小角落上跑得对。我在自己刷题和看别人代码时都会优先看这些边界条件它们往往能看出一个人是真懂还是背答案。4. 实战中的易错点、相似题与面试技巧4.1 我踩过的坑先说我自己刷这道题时踩过的坑每一个都在代码里真实发生过。你能避开一个是一个。第一个坑把min写成了max。我第一次做这题的时候满脑子都是“要找到两条最大高度的线”然后写出来的面积公式是max(height[i], height[j]) * (j - i)。结果一跑示例就发现不对因为两根线一根 1 一根 100我的公式算出来是 100 乘以距离可实际上水早就从 1 那边漫出去了。这个错误属于“题意理解偏差”不只是手误。你要时刻提醒自己容器高度永远由矮板决定高板再高也只是锦上添花不能雪中送炭。第二个坑移动了高板而不是矮板。这个更隐蔽因为代码语法完全没毛病可结果就是不对。为什么因为移动高板之后容器高度不会变高宽度反而变小面积只会小于或等于当前值。我去翻了很多博客发现不少人把“移动矮板”写成了“移动高度更低的那根”其实是一个意思但潜意识里容易搞反。我自己的经验是在代码注释里写上// 短板决定天花板移动短板这样下次回看时不会犯迷糊。第三个坑相等高度时没有明确分支导致某种极端情况死循环。比如我把条件写成if height[i] height[j]: i 1 if height[i] height[j]: j - 1两边相等时两个if都不执行指针原地不动死循环。这种 bug 在面试时特别容易紧张写出来。正确的做法是用if / else保证每次循环必然移动某一个指针。这一点在 3.1 节的代码里已经体现出来了。第四个坑忘了更新ans。别笑真有人会在移动指针之后再更新答案导致漏掉一些状态。比如先if height[i] height[j]: i 1然后才ans max(ans, area)那计算用的i已经变了完全不是当前容器。更新答案必须和指针移动保持“先算后移”的顺序。第五个坑初始化ans为-1或者None。虽然这道题ans 0就够用但有些人在别的题里习惯了初始化为负无穷拿到这里来用也没问题只是没必要。我见过有人初始化ans float(-inf)代码没毛病但可读性差。简单题就用简单的初始化。4.2 手推一遍完整过程文字说再多不如亲手跑一遍例子。我拿力扣官方的示例来走一遍height [1, 8, 6, 2, 5, 4, 8, 3, 7]。这个例子比较经典因为最终答案是 49对应的是下标1和8这两根高度为 8 和 7 的线。初始i 0j 8ans 0。我用表格记录每一步的状态步骤ijheight[i]height[j]widthareaans移动方向10817888height[0] height[8]i2188774949height[1] height[8]j--3178361849height[1] height[7]j--4168854049height[1] height[6]j--5158441649height[1] height[5]j--6148531549height[1] height[4]j--713822449height[1] height[3]j--812861649height[1] height[2]j--911----49i j循环结束最终输出ans 49。这个手推过程很有信息量。你可以看到最优解在第二步就找到了后面所有步骤都是在“证明其他组合不会更优”。第 4 步height[1] height[6]时走了j--分支这是我代码里的标准行为结果也没漏掉答案。如果你自己在纸上推演注意一点area是按“当前面积”记录的不是按“历史最大”记录的。我这张表里的area列是每一步直接算出来的面积ans列才是历史最大值。很多同学弄混这两个概念导致最后返回值不对。4.3 相关题目串联记忆第11题不是孤立的。刷题最忌讳一题一题地背学会串联才有效率。我把和它相关的几道题放在一起对比一下题目核心思路与第11题的关系11. 盛最多水的容器双指针从两端向中间收窄移动短板本题42. 接雨水按列计算找左右最大高度可用双指针/单调栈同样关注“短板”效应但每列的水量由两侧最大值决定84. 柱状图中最大的矩形单调栈找左右边界关注的是柱子高度和跨度但核心也是“找最优跨度”167. 两数之和 II - 输入有序数组双指针根据和与目标的大小关系移动同样是双指针同样是根据单调性排除搜索空间15. 三数之和排序后固定一个数剩下两个用双指针双指针的经典应用搜索空间压缩思路一脉相承这里面最像的是 42 题“接雨水”。面试中经常出现“你先做第11题再给我说说第42题”这种套娃式考法。接雨水比盛最多水更复杂因为前者要把每一列能接的水都加起来后者只需要找到一个全局最大值。但两者有一个共同点都依赖“短板决定水位”的直觉。你如果能把第11题的证明讲透接雨水的双指针解法也会好理解很多。还有一类变体是“盛最多水的容器升级版”比如把二维容器变成三维的“接雨水 II”力扣407题那道题就要用到优先队列 BFS 了难度直接上了一个台阶。建议先把二维的彻底搞定再考虑三维。4.4 面试里怎么把这题答成加分项面试和平时刷题有一个很大的区别面试官不只想知道你会不会做更想知道你是怎么思考的。所以答题节奏很重要。我的建议是四步走。第一步先复述题意并确认约束。比如你可以说“我理解这个容器的高度由较短的那根线决定宽是两个下标的距离要返回最大面积。请问数组长度有什么限制吗”这一步能展示你的沟通能力和需求确认习惯很多面试官会暗自加分。第二步先给暴力解再过渡到优化。不要一上来就甩双指针。你可以说“最直接的办法是枚举所有下标对复杂度 O(n^2)。但 n 到 10^5 的话会超时我想想能不能优化。”这样面试官看到的是你“从朴素到高效”的思考过程而不是背题。第三步给出双指针解并主动证明。写完代码后不要等面试官问直接说“我解释一下为什么移动短板不会漏解。当 height[i] height[j] 时以 i 为左边界的任何容器宽度都小于当前宽度而且高度不会超过 height[i]所以面积必然小于当前面积于是 i 可以安全右移。”这段话一出来基本就已经把这题答成加分项了。第四步主动讨论边界和扩展。你可以补充“如果数组长度小于 2 我直接返回 0如果高度相等移动任意一侧都是安全的因为证明里的不等式依然成立另外我用的是 O(1) 额外空间时间复杂度是 O(n)。”最后如果时间允许再提一句这题和接雨水的区别。面试官问“你现在用的是什么语言”这种问题时不要只回“Python”你可以顺手说一下“Python 的话整数没有溢出问题所以直接用 int 就行如果是 Java 或 C我会关注一下面积是否会超出 int 范围”。这种细节是经验积累出来的也是让面试官觉得你“真的写过代码”的关键。最后再分享一个私人的刷题习惯遇到这种“看起来像 DP其实是双指针”的题我会在笔记里单独开一个分类叫“搜索空间压缩”。每次做完我除了写题解还会在下面写一句话总结这次的核心收获。第11题我写的是“两个指针从两端开始每一步排除一个不可能成为最优解的边界。证明靠的是不等式宽度变小高度被短板锁死。”这句话后来帮我秒解了好几道双指针题。第11题刷三遍第一遍看题解理解思路第二遍合上书默写第三遍讲给别人听。讲得清楚才算真会。我自己带过几个朋友刷题亲眼看着他们从“看不懂证明”到“能给我讲明白”转折点都是在于亲手在纸上推导了一遍那个简单的不等式。希望这篇刷题笔记也能帮你省下一点自己摸索的时间。