ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

盛最多水的容器:双指针原理、证明与面试实践

2026/9/11 7:35:25 拓冰建站 浏览量
盛最多水的容器:双指针原理、证明与面试实践 1. 先读懂题它考的其实是一道“木桶效应”应用题1.1 题目描述与最容易忽略的隐含条件先把这个题的题面原样摆出来给你一个长度为 n 的整数数组 heightheight[i] 表示在坐标 (i, height[i]) 处竖着一根垂直线。现在要你在这个坐标系里任意选两根线让它们和 x 轴一起围成一个容器问这个容器最多能装多少水。很多人第一次看到这个题第一反应是“这不就是求两根柱子之间围成的矩形面积吗”对但不完全对。容器能装多少水不是由两根柱子里高的那根决定的而是由矮的那根决定的。换句话说两根柱子高度分别是 3 和 8中间距离是 5那么能装的水量就是 3 × 5 15。哪怕那根 8 的柱子再高也没用水会从 3 那一侧漫出去。这个逻辑放到生活里就是典型的木桶效应一个木桶能装多少水取决于最短的那块木板而不是最长的那块。所以这道题真正在考的数学模型是在数组里找两个下标 i 和 j让min(height[i], height[j]) * (j - i)这个值最大。其中j - i是两根柱子的水平距离也就是容器的底边长度。我拿一道面试的真实场景说说。LeetCode 官方给出的示例是height [1,8,6,2,5,4,8,3,7]答案是 49。这个示例选的是下标 1 和下标 8也就是高度 8 和高度 7 两根柱子水平距离是 7所以面积是min(8,7) * 7 49。很多人一开始看答案会以为选的是最高的那根 8 和另一根 8也就是下标 1 和下标 6距离只有 5面积是8 * 5 40反而小了。这就是这个题最反直觉的地方并不是柱子越高越好还要考虑它们之间的水平距离够不够远。1.2 为什么这道题能成为面试高频题我在面试里出这道题其实不只是因为它被 LeetCode 收录了。它是一道非常典型的“中等难度”题目它有很清晰的暴力解法也有很漂亮的最优解法而且两种解法之间的思维跨度恰好能反映出一个候选人真实的算法功底。如果候选人只能写出暴力解那至少说明他具备最基础的枚举能力能看懂题、能写出可运行的代码。如果候选人能在提示一句“能不能不用两层循环”之后自己推导出双指针解法那说明他脑子里是有数据结构与算法的整体图景的知道什么时候该用什么样的套路去优化。这种差距比背一百道题要真实得多。另外这道题的知识点范围很干净不会牵扯到堆、树、图这些复杂数据结构。它只考数组、双指针、还有一点点贪心思想。范围小不代表简单正因为涉及的概念少反而能更精确地看出一个人对“为什么这个解法是对的”这件事有没有想清楚。很多人能默写出双指针代码但一追问“为什么移动矮的那一根不会漏掉最优解”就卡住了。这恰恰是面试官最想挖的地方。1.3 暴力解先让思路跑通不管面试还是刷题我的习惯永远是先把暴力解写出来。一方面是给自己一个保底方案另一方面是用它来验证后续优化解法的正确性。暴力解的逻辑很简单把数组里任意两根柱子的组合都算一遍面积取最大值。这个没有任何技巧两层循环直接枚举def max_area_bruteforce(height): n len(height) res 0 for i in range(n): for j in range(i 1, n): cur min(height[i], height[j]) * (j - i) res max(res, cur) return res这段代码的时间复杂度是 O(n²)空间复杂度是 O(1)。在数组长度小的时候完全能跑但一旦 n 超过 10 万这个代码基本就废了。面试官让你做这道题等的是你把 O(n²) 优化到 O(n)而不是看你能不能在同一个复杂度里抠那一点点常数。暴力的意义还在于它可以当作测试基准。你后面写双指针解法的时候可以随机生成一堆小数组把两个函数的输出对拍一下这是最可靠的正确性验证手段。我写算法题一直保留这个习惯后面也会再强调。2. 双指针解法为什么移动“矮柱子”一定不会错过最优解2.1 从最宽的容器开始双指针的基本框架双指针是一种直觉上很自然的优化既然我们要找两根柱子让面积最大而面积等于min(height[i], height[j]) * (j - i)那我们可以先让底边最长也就是一开始左指针指在下标 0右指针指在下标 n-1。这时候水平距离是最大的但容器高度可能不高。接下来怎么办我们想让面积更大只有两种可能要么变得更高要么水平距离不要缩得太小。但水平距离在指针相向移动的过程中只会越来越短所以唯一的希望就是柱子高度能变大。也就是说每次移动指针时我们要保留较高的一根柱子放弃较矮的一根柱子去尝试能不能在更靠里的位置找到更高的柱子来弥补距离的损失。于是代码骨架就出来了左指针 left 初始化为 0右指针 right 初始化为 n-1。计算当前两根柱子围成的面积更新最大面积。比较 height[left] 和 height[right]。左指针的柱子更矮就 left 加 1右指针的柱子更矮就 right 减 1。直到 left 和 right 相遇返回最大面积。这个流程非常简单但核心问题在于为什么移动较矮的那根就一定是安全的万一正确答案藏在当前这根矮柱子和另一根柱子之间呢2.2 严谨版证明为什么移动较矮的一侧是安全的我用数学的方式说清楚这一点这也是面试追问时最好用的回答思路。假设当前左指针指向下标 L右指针指向下标 R且 height[L] height[R]。那么当前容器的面积是height[L] * (R - L)因为容器高度受制于较矮的左柱子。现在我问一个问题如果把右指针往左移动也就是尝试所有右指针在(L, R)之间的下标 k那么由 L 和 k 组成的容器面积会是多少它的面积是min(height[L], height[k]) * (k - L)因为 k 肯定小于 R所以k - L R - L。同时min(height[L], height[k])一定小于等于 height[L]。因此这个面积一定小于等于height[L] * (R - L)也就是一定小于等于当前面积。换句话说只要左指针保持在 L 这个位置无论右指针怎么收面积都不可能超过当前这组 L 和 R 形成的面积。既然当前左指针已经把它能贡献的最大面积算完了那再继续留着 L 就没有意义了只有把 L 往右移让它离开这个位置才有可能通过换一根更高的柱子来创造惊喜。反过来如果 height[L] height[R]那就对称地说明只要右指针保持在 R无论左指针怎么向右收面积都不可能超过当前面积。所以此时应该把右指针向左移动。这个证明最核心的一点是“当前较矮的那根柱子已经达到了它所能达到的最大宽度的极限”。它不是被随便放弃的是带着“当前面积已经是它作为瓶颈时理论上的最大值”这种结论被放弃的。这个结论比背口诀重要得多因为面试官追问的时候考的就是你能不能当场说出这一步。2.3 正确性验证收窄区间但不漏答案光有上面的证明还不够我建议你在理解之后再自己跑一遍一个简单例子感受一下“为什么过程中会错过一些组合但不影响最终答案”。比如数组[2, 5, 4, 8, 3]。初始 L0R4height[0]2height[4]3面积是2 * 4 8。因为左边更矮所以 L 右移到 1。此时 L1R4height[1]5height[4]3面积是3 * 3 9。因为右边更矮R 左移到 3。此时 L1R3height[1]5height[3]8面积是5 * 2 10。继续移动较矮的左边L2R3面积是4 * 1 4最终最大值是 10。你回头检查一下左边那根高度为 2 的柱子有没有可能是最优解的一部分它和右边最远的柱子组合面积是 8而它和任何中间柱子组合距离更短高度也被它自己限制在 2所以最多也就是2 * 4 8不可能是 10。这就是那个“安全”的含义我们确实没有枚举到所有组合但所有被跳过的组合都被证明不可能成为最优解。这个证明思路还可以这样理解每一次移动指针本质上是把“当前指针所指的那根柱子”从候选集里永久淘汰。淘汰它不是拍脑袋而是基于它作为较矮一侧时目前这个最大宽度已经锁死了它的面积上限。这个淘汰逻辑在每一步都成立所以最终留下的最值一定是全局最值。2.4 复杂度分析为什么一定是 O(n)空间复杂度不说了只用两个变量和几个常数是 O(1)。时间复杂度要注意看循环条件。双指针解法里left 只增不减right 只减不增两者相遇时循环结束。每次循环只移动一个指针所以总的迭代次数最多就是 n 次。也就是说时间复杂度是 O(n)比暴力的 O(n²) 整整降了一个数量级。这里有个面试爱问的点有人会误以为每次移动一根指针两个指针都可能移动所以复杂度是 O(2n)其实常数 2 在渐进分析里不算数O(2n) 还是 O(n)。但如果你把这个话说出去反而显得你不懂什么叫渐进复杂度。面试时直接说“线性时间内遍历完成总体 O(n)”就够了多余的话一句都不用加。3. 手写代码与关键细节3.1 代码实现代码本身很短但短不代表好写。我先把最终版放出来def maxArea(height): left 0 right len(height) - 1 max_area 0 while left right: current_height min(height[left], height[right]) current_width right - left max_area max(max_area, current_height * current_width) if height[left] height[right]: left 1 else: right - 1 return max_area别看才十几行里面有几个细节值得单独拎出来说。第一循环条件是left right不是left right。如果允许 left 和 right 相等那两根柱子变成同一根容器宽度为 0面积也是 0没有任何意义而且还会造成多余的比较。实际代码里写也不会报错但逻辑上不严谨。第二计算面积用的是min(height[left], height[right])不是height[left] if height[left] height[right] else height[right]这种写法。上面这种写法更简洁而且自动处理了 height 为 0 的情况。第三移动指针的判断标准和面积计算用的“矮的那个值”是同一个逻辑。但要注意你判断的是原始高度不是计算面积后的任何中间量。有些初学者会在计算面积后顺手把较矮的一根“跳过”这在某些变体里可能加速但会引入边界风险我先不建议这样写。第四当两根柱子高度相等时我们的代码走的是else分支也就是右指针左移。这其实是任意的。为什么因为高度相等时无论移动哪一边当前面积都已经用到了两根柱子的上限。移动左边后新的左柱子如果更高那右柱子作为瓶颈仍然存在但距离变短面积不会超过当前值除非新左柱子低到改变瓶颈但那样面积更小。所以相等时移左移右都不影响最终结果。面试时如果你能把这个结论也顺手说出来是很加分的。3.2 从运行过程看代码为什么这么写我用示例[1,8,6,2,5,4,8,3,7]手推一遍你对照代码看会非常清晰轮次leftrightheight[left]height[right]面积max_area108171×888218877×74949317833×61849416888×54049515844×41649614855×31549713822×2449812866×1649注意第 2 轮里左指针已经指向了高度 8 的那根柱子右指针指向高度 7 的柱子得到 49。后面右指针一直往左收面积始终没有超过 49最终返回 49。这里有个很反直觉的现象第 4 轮两根柱子都是 8距离是 5面积是 40明明两个柱子都很高却不如第 2 轮的一根 8 和一根 7 来得大。原因就是距离少了 2而容器高度没有提升。这再次验证了这个题的本质高度和距离是互相制约的两个变量不是单纯地追求某一个极端。3.3 快速改成其他语言时要注意什么Java 版本基本是照搬但要注意数组长度别每次循环都调height.length虽然现代 JIT 会优化但写成变量更干净class Solution { public int maxArea(int[] height) { int left 0; int right height.length - 1; int maxArea 0; while (left right) { int h Math.min(height[left], height[right]); maxArea Math.max(maxArea, h * (right - left)); if (height[left] height[right]) { left; } else { right--; } } return maxArea; } }JavaScript 和 C 也基本一致主要区别在类型声明和语法。这里有一个我要强调的共性坑在 C 里height[left] height[right]如果写成会改变指针移动方向吗不会但对代码可读性没有提升反而有人会困惑。我的建议是严格按if (左边 右边) 左移; else 右移;来写这个分支结构简单清晰不需要为了“相等时哪边都行”这种话去故意加一些花活。另外在 JS 里要注意height可能是很长的数组但双指针解法本来就不会爆栈也没有递归所以不用担心调用栈问题。真正需要担心的是面试现场手写时把while写成for导致索引越界。4. 边界情况、易错点与面试官追问清单4.1 边界情况速查表面试里写完代码面试官一定会让你过一遍测试用例。数组题目的边界情况其实很固定空数组、单元素、双元素、全零、全相同、单调递增、单调递减、极大极小值。输入预期输出原因[]或[3]0少于两根柱子无法构成容器[4, 9]4只有一种组合宽度是 1高度是 min(4,9)4[0,0,0]0高度全为 0面积永远是 0[5,5,5,5]15任意两根组合最大宽度是 3高度是 5[1,2,3,4,5]6单调递增时最大面积往往出现在第一根和最后一根之间1*442*333*26实际上需要手算[1,5]4、[2,5]6、[3,4]6、[4,5]4最大值 6。这里说明单调并不代表只有两端有用[5,4,3,2,1]6对称情况最大值也是 6你可能会发现单调递增或递减时最大面积不一定出现在两端。因为虽然距离最大但高度是线性变化的。这类测试用例最容易暴露出“以为双指针只要移矮的就稳了”这种理解误差——移矮的是正确的但你必须理解“为什么移”而不是机械地执行。4.2 常见易错点我见过好几个人把代码写成这样结果测试用例只有部分能过# 错误示范 while left right: area min(height[left], height[right]) * (right - left) max_area max(max_area, area) if height[left] height[right]: left 1 if height[left] height[right]: right - 1这个写法有两个问题。第一第二个if不是elif可能导致一轮循环里同时移动两个指针跳过了某些本应检查的组合。第二当两个if都成立时left 已经变了再拿新的height[left]去和旧的height[right]比逻辑已经混乱了。其他常见错误包括面积公式写成max(height[left], height[right]) * (right-left)这直接违背了“木桶装水看短板”的定义初始化left0, right0导致循环根本不进去把height数组当成有序数组处理还有的人在移动指针时用了while内层循环连续跳过多个相同高度的柱子导致指针直接越过边界。这里我想多说一句连续跳过相同高度的柱子的优化并非完全不可取某些特殊情况下确实能减少比较次数。比如数组是[5,5,5,5,5,5,5,5,5,8]从左往右看全是重复的 5每次移动一次指针确实浪费。但如果要写这种优化你必须非常小心地处理“跳过后的位置可能已经越界”和“跳过过程中是否错过了某个更优组合”这两个问题。在面试现场除非你足够熟练否则我不会推荐这种花活老老实实每次移动一步最安全。4.3 面试官可能会怎么追问这个题最容易被追问的点我觉得有四个。第一个为什么移动较矮的柱子不会漏掉可能的最优解这个问题我在前面已经给出了完整证明不再重复。关键是你能不能当场用“瓶颈已经达到理论极限”这句话把逻辑讲清楚。第二个如果数组长度特别大内存装不下怎么办这个问题有点偏系统设计双指针解法本身只用 O(1) 空间所以不会因为数组过大而增加内存压力。但如果数组是流式输入你不能同时拿到全部数据那就没法直接用双指针了。碰到这种追问你起码要知道流式场景需要换一种思路比如维护一个左侧最大高度和右侧最大高度的辅助结构或者用分块处理具体要看能否接受精度或近似。第三个如果要返回能盛最多水的两个柱子的下标而不是只返回水量怎么改这个非常简单在更新max_area时顺手把left和right记下来即可。Python 里可以额外维护ans_left和ans_right两个变量最后返回它们。第四个如果把题目改成“盛最多水的容器但是容器底部不是水平的”也就是把数组想象成地形剖面盛水的量会怎样变化这个其实就是接雨水那道题的一部分。你如果把这个题和接雨水一起准备面试时就形成一个完整的知识块了。5. 相关题型串联与备考建议5.1 双指针题型的共性套路做题多了以后你会发现双指针并不是一种具体的算法而是一类场景下的解题框架。凡是遇到“在一个数组/字符串里要找两个下标使得某个由这两个下标构成的表达式最大/最小/满足某个条件”这种题目都可以优先考虑双指针。常见的可以归入这个框架的题目有三数之和排序后固定一个数另外两个数用双指针在区间内寻找。最接近的三数之和同样是排序后双指针。盛最多水的容器就是本题。接雨水这是双指针的进阶用法要用 left_max 和 right_max 配合。长度最小的子数组滑动窗口本质上也属于双指针的变体。最小覆盖子串同样是滑动窗口。这些题如果一个个孤立地刷很容易觉得每道题都是新知识但只要整理成“双指针/滑动窗口”这个专题你会发现它们共享同一套思维模型两个指针维护一个区间通过调整左右端点的位置来逼近答案。区别只在于指针移动的条件不同、区间需要满足的性质不同。5.2 从盛水到接雨水一道题延伸出的完整知识块接雨水和盛最多水的容器经常被搞混我来帮有需要的朋友理一下。盛最多水的容器是让你“选两根柱子围一个容器”容器是虚拟的中间没有东西挡着水也不会漏到别的地方去。接雨水则是把整个数组看成一个地形剖面当雨水降下来之后有多少水会积在数组本身形成的坑洼里。接雨水的经典解法之一是把每个位置能接的雨水量计算出来这个位置左侧最高的柱子和右侧最高的柱子中较低的那个减去当前位置高度如果大于 0就是当前位置可以接到的雨水量。这个逻辑用双指针写起来也很顺维护 left_max 和 right_max哪边矮优先处理哪边。两道题的共同点在于都要用“短板”来决定最终结果都要用双指针实现 O(n) 复杂度。区别在于盛水题只需要一个全局最大面积而接雨水题需要逐位累加。如果你能把这两道题对比着刷对双指针和“贪心思想”的理解都会更深一层。5.3 刷题之外的一点心得这道题我用不同语言写过不止十遍每次给学生讲课、给候选人面试我都会下意识地把这道题拿出来当典型。它教会我的不是“这个答案要背下来”而是“当一个问题的短板可以被明确识别时它的优化方向往往就藏在短板里”。最后的最后如果你正在准备面试我建议你做这么一件事把这题的代码从 Python、Java、C 三种语言各写一遍然后用随机生成的大数组去验证确保逻辑没有因为语言差异而写错。写作顺序上先在白纸上画一遍指针移动的过程再动键盘。等你能够用两分钟把双指针思路和“为什么移矮的”这个证明流畅讲出来这道题才算真正吃透了而不是背会了。