ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LeetCode 11 盛最多水的容器:双指针优化算法详解

LeetCode 11 盛最多水的容器:双指针优化算法详解 1. 题目拆解盛最多水的容器到底在考什么这道题在LeetCode上是第11题也被收录在“热题100”里面。第一次刷到它的朋友很容易被题目里的“容器”“水”这些字眼带偏觉得它是个模拟题或者几何题结果绕了半天最后发现核心就一句话给你一个数组每个位置的高度代表一堵墙让你找出两根柱子使得它们和底部x轴围成的区域能装下的水最多。容器能装多少水不是看两根柱子里最高的那根而是看最矮的那根。这就是最经典的“木桶效应”——决定盛水量的永远是最短的那块木板。数组里每个元素就是墙的高度下标就是墙的位置两堵墙之间的水平距离就是容器的底边长度。面积公式很简单area min(height[i], height[j]) * (j - i)i和j是两根柱子的下标j必须大于i这样才能构成一个底边。题目要求的就是遍历所有可能的“两根柱子”组合找出能产生最大面积的那一组。这道题的难度标的是中等但它其实是个极其典型的“双指针”入门题。如果你刚刷完几道简单题看到这个题第一反应大概率是暴力枚举——两个for循环把所有组合都过一遍也能算出来但数组长度一旦到10万级别就直接超时了。这道题的核心价值就在这它逼着你去想“怎么减少不必要的计算”而不是“怎么把计算做完”。这是从暴力思维到优化思维的分水岭也是面试官特别爱考它的原因。我做这道题的时候有个很直观的感受它不像动态规划那样需要你抽象出状态转移方程也不像回溯那样需要画递归树。它的整个优化过程非常“直观”只要你理解了“为什么要移动较短的那根指针”整个思路就顺下来了。接下来我会把从暴力到双指针的完整思考路径拆开讲确保零基础的人也能跟着推一遍。2. 核心思路拆解为什么是双指针而不是别的2.1 暴力解法先知道最笨的办法是什么先看一眼暴力解法长什么样这样后面优化起来才有对照。常规的双层循环是这样的def maxArea(height): n len(height) max_area 0 for i in range(n): for j in range(i 1, n): area min(height[i], height[j]) * (j - i) max_area max(max_area, area) return max_area逻辑没有任何毛病就是枚举所有下标组合算面积、更新最大值。时间复杂度O(n²)空间复杂度O(1)。数组长度是三位数的时候跑起来毫无压力但LeetCode上这题给的数据范围是n最大到10万O(n²)就需要10的10次方次操作Python直接跑到天荒地老。暴力解法的最大问题在于**它计算了大量根本没有希望成为最优解的组合。**比如你左边选了根矮柱子右边不管选多高盛水量都被这根矮柱子卡死了可暴力循环还是会一个不漏地算过去。能不能想办法跳过这些明显不可能的分支这就是优化的入口。2.2 双指针的诞生从两端向中间收缩双指针的做法非常优雅用两个指针分别指向数组的最左端和最右端计算当前面积然后每次把高度较小的那一端往中间挪一步重复这个过程直到两个指针相遇。为什么从两端开始因为j - i的最大值就是n-1底边最长的时候就是两端这是面积公式里能取到的最大宽度。然后我们通过收缩慢慢缩小底边长度同时试图找到更高的柱子来弥补宽度的损失。核心规则就一句话每次移动较矮的那端指针。为什么因为如果你移动较高的那端柱子新的组合高度不可能超过原来较矮的那根柱子——高度被矮柱限制宽度还在缩小面积只可能变小或者持平绝不可能变大。所以移动高柱子是纯亏的事情直接放弃。反过来移动矮柱子虽然宽度变小了但新柱子有可能比原来的矮柱子更高这样容器的整体高度就有机会提升面积存在变大的可能性。这个“移动较矮端”的策略本质上就是贪心——每一步都放弃一部分绝无希望的区域只保留可能存在更大面积的搜索空间。2.3 用一次具体推演验证双指针正确性我拿个具体例子手动推一遍数组是[1,8,6,2,5,4,8,3,7]这也是题目自带的示例。初始化左指针i指向下标0高度1右指针j指向下标8高度7。底边宽度8高度是min(1,7)1面积8。当前面积记下来。移动哪边左边高度1右边高度71更矮把i往右挪到下标1高度8。这时候底边宽度7高度min(8,7)7面积是49。一下子大了非常多。再比较左边8右边7右边矮把j往左挪到下标7高度3。底边宽度6高度min(8,3)3面积18不如49不更新。左边8右边3左边高移动右边j到下标6高度8。底边宽度5高度min(8,8)8面积40还是不如49。现在两边都是8随便移动哪边理论上都行。按平时习惯可以移动左边i到下标2高度6。宽度4min(6,8)6面积24移动右边也行j到5高度4宽度4min(8,4)4面积16。后面继续算下去最终得到的最大值就是49。这个推演过程里有个特别值得注意的点当两个指针高度相等时移动哪边其实都不会影响最终答案的正确性。为什么因为在这种状态下无论移动哪边宽度都在减少而高度上限不可能超过当前这个相同的高度值假设这个高度是h你移动任何一边后新组合的高度最多也只有h除非新柱子更高但更高的柱子如果存在它之前一定已经被纳入过某个更宽的区间里被计算过了。这个结论初看有点绕但在LeetCode的讨论区里已经有很多数学证明核心就是双指针剪枝掉的区间面积必然不会超过当前已找到的候选值。3. 代码实现与细节剖析从 Python 到 C3.1 Python 版本最简洁的写法class Solution: def maxArea(self, height: List[int]) - int: left, right 0, len(height) - 1 max_area 0 while left right: h min(height[left], height[right]) w right - left max_area max(max_area, h * w) if height[left] height[right]: left 1 else: right - 1 return max_area这一段代码基本是全网最标准的双指针解法逻辑非常紧凑。有几个细节值得说第一max_area理论上初始化为0就行因为所有面积都是正数高度和宽度都至少为1。第二当height[left] height[right]时代码走的是else分支也就是右指针左移。刚才讲过这里移动哪边都不影响正确性走哪个分支纯属个人癖好。第三整个循环没有显式的“提前退出”条件就是老老实实让两个指针相撞。因为最坏情况下指针要从两端一直挪到中间一共n-1步复杂度是严格的O(n)。3.2 C 版本性能敏感场景下的标准答案有些朋友会在面试的时候用C写这题我也把常见写法贴出来class Solution { public: int maxArea(vectorint height) { int left 0; int right height.size() - 1; int max_area 0; while (left right) { int h min(height[left], height[right]); int w right - left; max_area max(max_area, h * w); if (height[left] height[right]) { left; } else { --right; } } return max_area; } };C版本和Python版本几乎一模一样唯一需要注意的就是不要用height.size()直接和int做比较时产生符号警告最好先存一下长度不过这里用right height.size() - 1因为size_t是unsigned如果数组为空会出问题。但题目保证n2所以没问题。3.3 复杂度的本质一次遍历的代价双指针版本的复杂度很好算时间复杂度O(n)。两个指针一左一右每次循环移动一步相遇时一共走了n-1步不存在嵌套循环也没有额外遍历。空间复杂度O(1)。只用到了几个整数变量没有创建数组或者哈希表之类的结构。这个复杂度是这道题的最优解了吗是的因为你要确认最大面积至少需要看一眼每一根柱子的高度所以O(n)是下界。这也是为什么这道题能成为双指针经典题——它的最优解简单、高效、思路清晰几乎没有任何冗余。4. 常见错误与排查实录我在提交时踩过的坑4.1 误以为要用单调栈网上有不少题解提到“接雨水”那道题用单调栈有些朋友做这道题的时候也想当然地上了单调栈然后发现极其绕。这题跟单调栈场景有个本质区别接雨水是求所有凹槽的总水量需要考虑每个位置左右两边高度情况所以单调栈/前缀最大值的路子更合适盛最多水的容器是求两根柱子的最大组合面积它天然就是双指针的菜。不要一看到“水”“容器”就往接雨水的方向想虽然标题长得像但解法路径完全不一样。4.2 移动了较高的指针这是新手最容易犯的错误。我一开始自己写的时候也是随手就让右指针往左移结果答案错得很离谱。后来想明白了你移动高指针意味着主动放弃了当前能构成的最大宽度同时高度还被矮指针锁死新的面积不可能比当前面积更大。这一步操作是纯亏的唯一合理的做法是尝试把矮的那一端换掉。可以这么理解你手里有一块又短又宽的木板和一块又长又窄的木板想让水桶装更多水你会去换哪块当然是换短的那块因为长的那块不是限制因素。4.3 宽度和高度计算顺序搞混有些写法是先更新指针再算面积这种就会漏掉最两端的组合。记住**每次移动之前先把当前左右指针指向的两根柱子构成的面积算掉然后再移动。**顺序不能反。这题的示例里最优解是下标1和下标8也就是数组的第二个和最后一个元素如果一开始就直接移动指针这个组合可能直接就被跳过了。4.4 相等高度时犹豫不决两个指针高度相等的时候移动左还是右前面分析过结果都一样。但很多人在写代码时会在相等分支里多加一个判断导致逻辑分支变多还容易出bug。我建议直接把它归到else分支里代码简洁行为也无懈可击。5. 进一步思考这道题还能怎么变着考5.1 变体一输出最大面积对应的柱子下标面试官可能会追问不仅要最大面积还要返回是哪两根柱子。实现上只需要在更新max_area的时候顺手记录一下left和right就行。不过要注意如果存在多个面积相同的组合题目要是没规定返回哪个你就自己定个策略比如返回第一次出现的。5.2 变体二数组环状排列怎么办如果柱子围成一个环也就是首尾相接那问题就从“找两根柱子”变成了“在环上找两根柱子”。处理方式通常是把数组复制一倍接在后面展开成线性的来跑双指针。这种变体在面试里出现频率不高但真出现了以上思路能直接给你兜底。5.3 变体三求所有面积大于等于K的柱子对数量这个就麻烦一些了不是简单双指针能解决的需要排序加二分之类的技巧。它考的是对双指针模型的迁移能力。如果这道题你已经刷透了可以尝试自己想一下这个变体对思维的锻炼会很大。5.4 与相邻题目的关联做完这道题之后强烈建议紧接着去做LeetCode 42“接雨水”和LeetCode 15“三数之和”。这两道题都有双指针的影子但侧重点不同。接雨水需要你理解“每个位置能接多少水取决于左右最大高度的较小值”三数之和需要你在排序后的数组上左右夹逼。三题连刷你对双指针的理解会有一个质的飞跃。6. 面试场景下的表达话术与加分技巧这道题出现在面试中的概率非常高尤其是字节、腾讯、阿里这类算法考核比重大的公司。面试时除了把代码写对面试官更在意你“有没有分析过程”。我的建议是按以下话术组织表达先讲暴力解“我先想到的是枚举所有i和j算面积更新最大值复杂度O(n²)。”再讲优化突破口“观察面积公式发现宽度减小的情况下面积要变大只能靠高度提升。固定较矮的柱子时移动较高的柱子不可能让高度提升所以这个方向可以直接剪枝。”最后讲双指针方案“所以用两个指针从两端向中间移动每次移动较矮的一侧保留可能产生更大面积的方向复杂度降到O(n)。”面试官一般听完这几句就会觉得你思路清晰。如果他还想深挖可能会问“为什么移动较矮的一定不会漏掉最优解”你可以把第2.3节那个推演过程简单复述一遍关键在于说明“被剪枝的区间里任何一个组合的面积都受限于当前矮柱高度且宽度还要更小所以不可能超过当前候选值”。另外写代码之前先和面试官对齐思路也是很好的习惯。你可以问一句“我准备用双指针从两端向中间移动您看可以吗”既显得你沟通意识好也避免自己闷头写完发现方向错了。7. 实测表现与刷题建议我在自己电脑上用Python跑了一下这题的极端情况数组长度为10万所有高度都是1也就是最大面积是99999的那种极端数据双指针版本耗时不到0.01秒暴力版本我根本不敢跑完。这个差距就是O(n)和O(n²)的真实体感差距。刷题节奏上我建议这道题放在你开始刷“热题100”之后的第三天左右做。前两天先做几道数组简单题比如两数之和、合并两个有序数组先熟悉一下数组的基本操作。然后直接上这道题体会一下“从暴力到优化”的转变。做完之后立刻去刷接雨水这样就形成了一个“双指针专题”的小循环记忆会更牢固。如果你是在校生准备秋招这道题可以说是必刷中的必刷。我在面试中遇到过它两次每次都出现在一二面的算法环节而且面试官都会追问优化思路。把这道题吃透相当于给双指针类题目打了个底子后续遇到类似题目你会觉得轻松很多。还有一个小技巧刷的时候不要只看题解就过最好自己先在纸上画一画模拟指针移动的过程把每一步的面积变化写出来。这个过程看似笨拙但真的比直接看代码有效得多。我见过很多同学代码背得滚瓜烂熟面试时换了个数组长度或者改了个条件就懵了根源就是没有真正理解指针移动的底层逻辑。这道题的核心收获不在于记住双指针这个名词而在于体会“如何通过排除不可能区域来减少计算量”的思维方式。这种思维模式比你多刷一百道题都值钱。
返回列表