力扣 11. 盛最多水的容器:双指针为什么只移动短板?
这道题是双指针里的经典题。
很多人能记住代码,但一到面试里就解释不清楚:为什么每次只移动较短的那一边?
这篇文章就从知识点出发,把这道题完整讲透。
一、先别急着做题,先补一个核心知识点
在正式讲题之前,先理解一个非常重要的结论:
一个容器最多能装多少水,不取决于更高的那块板,而取决于更短的那块板。
为什么?
因为水位不可能超过短板。
哪怕另一边再高,只要短板不够高,水就会从短板那一边流出去。
所以,容器的容量公式是:
面积 = 宽度 × 高度
在这道题里:
- 宽度 = 两根柱子之间的距离
- 高度 = 两根柱子中较短的那一根
所以可以写成:
area = (right - left) * min(height[left], height[right])
这就是整道题最核心的公式。
二、这道题考察的知识点有哪些?
这道题虽然不难,但很有代表性,主要考察下面几个知识点:
1. 双指针
双指针通常用于:
- 数组或字符串
- 从两端向中间逼近
- 在降低时间复杂度的同时保留有效信息
这道题如果暴力枚举所有两根柱子的组合,复杂度会是 O(n^2)。
而双指针能把它优化到 O(n)。
2. 贪心思想
每一步我们都要决定:
- 左指针动,还是右指针动?
这里并不是随便移动,而是有明确策略:
每次移动较短的那一边。
因为较长的那一边不是当前瓶颈,移动它没有意义。
3. 短板效应
这道题本质就是“短板效应”的应用:
- 容器容量由短板决定
- 想让容量变大,就必须尝试寻找更高的短板
- 所以每次只能舍弃当前较短的一边
这个思想非常重要,也是理解双指针正确性的关键。
三、题目描述
给定一个长度为 n 的整数数组 height,其中 height[i] 表示第 i 根柱子的高度。
从数组中选择两根柱子,和 x 轴一起构成一个容器,问这个容器最多能装多少水。
示例
输入:height = [1,8,6,2,5,4,8,3,7]
输出:49
解释:
- 选择下标
1和8 - 高度分别是
8和7 - 宽度是
8 - 1 = 7 - 容器高度取较小值
7
所以面积为:
7 * 7 = 49
四、先从最直观的暴力思路开始
最容易想到的办法就是:
- 枚举每一对柱子
- 计算它们组成的容器面积
- 取最大值
代码逻辑大概是这样:
max_area = 0
for i in range(n):
for j in range(i + 1, n):
area = (j - i) * min(height[i], height[j])
max_area = max(max_area, area)
这种做法当然能做出来,但问题也很明显:
- 一共有
n个位置 - 两两组合要枚举
O(n^2)次
所以时间复杂度是:O(n^2)
五、双指针优化:为什么能从 O(n^2) 变成 O(n)?
我们定义两个指针:
left = 0right = len(height) - 1
分别指向数组最左边和最右边。
这样一来,初始状态下的宽度最大。
然后我们一边计算当前面积,一边尝试缩小区间。
当前面积怎么计算?
area = (right - left) * min(height[left], height[right])
这个没有问题,关键在于:
计算完当前面积后,下一步该移动谁?
六、为什么只移动较短的一边?
这是整道题最关键的地方。
假设当前:
height[left] < height[right]
那么此时容器高度由谁决定?
显然是:
min(height[left], height[right]) = height[left]
也就是说,左边是短板。
当前面积是:
(right - left) * height[left]
现在如果你去移动右指针,会发生什么?
- 宽度一定变小
- 容器高度仍然最多受左边这个短板限制
- 即使右边变高,也没用,因为短板还是左边
所以移动右边,不可能让结果更优。
而如果移动左边,就有机会遇到一个更高的柱子:
- 虽然宽度变小了
- 但短板可能变高
- 面积才有可能变大
所以结论就是:
谁短就移动谁。
同理:
- 如果
height[left] > height[right],就移动right - 如果两边一样高,移动哪边都可以
七、把这个过程彻底讲明白
情况 1:左边更短
如果:
height[left] < height[right]
那么当前面积由左边决定。
这时候:
- 右边再怎么移动,都无法突破左边这块短板的限制
- 所以只能尝试移动左边,寻找更高的柱子
于是:
left += 1
情况 2:右边更短
如果:
height[left] > height[right]
那么当前面积由右边决定。
这时候:
- 左边再高也没用
- 真正限制容量的是右边
- 所以应该移动右边,尝试找到更高的柱子
于是:
right -= 1
情况 3:两边一样高
如果:
height[left] == height[right]
说明两边都是短板,移动任意一边都可以。
实际写代码时,通常直接归到 else 里面处理即可。
八、双指针解法完整代码
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
max_area = 0
while left < right:
width = right - left
h = min(height[left], height[right])
area = width * h
max_area = max(max_area, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
九、代码逐步讲解
1. 初始化左右指针
left = 0
right = len(height) - 1
一开始让容器宽度最大。
2. 记录最大面积
max_area = 0
用于保存遍历过程中的最优答案。
3. 循环条件
while left < right:
只要左右指针没有相遇,就还能形成容器。
4. 计算当前面积
width = right - left
h = min(height[left], height[right])
area = width * h
这里非常关键:
- 宽度由指针距离决定
- 高度由较短柱子决定
5. 更新答案
max_area = max(max_area, area)
保留历史最大值。
6. 移动短板
if height[left] < height[right]:
left += 1
else:
right -= 1
这是整道题的核心逻辑。
记住一句话:
移动高板没有意义,只有移动短板才可能得到更大的面积。
十、用示例手推一遍过程
数组:
height = [1,8,6,2,5,4,8,3,7]
第 1 次
left = 0,高度1right = 8,高度7
当前面积:
(8 - 0) * min(1, 7) = 8
左边更短,移动左指针。
第 2 次
left = 1,高度8right = 8,高度7
当前面积:
(8 - 1) * min(8, 7) = 7 * 7 = 49
更新最大值为 49。
右边更短,移动右指针。
后续继续移动
虽然之后还会产生很多组合,但都无法超过 49,最终答案就是:
49
十一、复杂度分析
时间复杂度
O(n)
因为左右指针每次至少移动一个,总共最多移动 n 次。
空间复杂度
O(1)
只使用了常数级额外空间。
十二、这道题最容易犯的错
1. 搞错面积公式
很多人会写成:
(right - left) * max(height[left], height[right])
这是错误的。
因为容器高度不是由高板决定,而是由短板决定。
正确写法一定是:
(right - left) * min(height[left], height[right])
2. 不理解为什么移动短板
这是这道题最常见的面试追问。
你一定要能说清楚:
- 宽度无论如何都会变小
- 所以想让面积变大,只能期待高度变大
- 而当前高度受短板限制
- 因此只能移动短板,去寻找更高的板
这才是双指针成立的根本原因。
3. 先移动指针,再算面积
有些人会把顺序写反:
- 先移动指针
- 再计算面积
这样会漏掉当前区间的答案。
正确顺序是:
- 先计算当前面积
- 更新最大值
- 再移动短板
十三、这道题背后的通用方法
这道题不只是背一个模板,它背后有一类很重要的思想。
1. 当答案由“两端关系”决定时,优先考虑双指针
如果一个问题:
- 和左右边界有关
- 可以从两端往中间缩小范围
- 每次缩小都能排除一部分无效解
那通常都可以想想双指针。
2. 贪心不是乱猜,而是有依据地舍弃
这道题每次移动短板,不是拍脑袋决定的,而是因为:
- 移动长板无法突破当前短板限制
- 所以长板对应的一整类情况都可以直接舍弃
这就是典型的贪心剪枝。
3. 先想清楚“什么因素限制了答案”
这道题里,真正限制面积的是:
- 宽度
- 短板高度
其中宽度一定会随着指针移动而减小,
所以唯一可能带来提升的,就是让短板更高。
一旦抓住这个限制因素,整个题目就通了。
十四、总结
力扣 11 是一道非常经典的双指针题,核心就一句话:
面积由宽度和短板共同决定,而宽度在缩小,所以只能通过移动短板去寻找更优解。
需要真正记住的不是代码,而是下面这套逻辑:
- 容器面积公式是
area = (right - left) * min(height[left], height[right]) - 每次先计算当前面积
- 谁短就移动谁
- 因为移动长板不会让答案变得更优
所以这道题最本质的结论就是:
双指针的关键,不是两边一起动,而是每次只移动限制答案的那一边。
十五、一句话记忆
盛水看短板,变大靠换短板。
如果你也是刚学双指针的算法小白~,这道题非常适合反复手推。
当真正理解“为什么移动短板”,双指针这类题就算真正入门了。