Container With Most Water 是 LeetCode Hot 100 里面一道非常经典的双指针题。
这道题看起来像是一道简单的数组题,但实际上它非常适合用来理解 双指针贪心移动 的核心思想。很多人第一次做这道题时,都会先想到暴力枚举:把每两个位置都试一遍,算出它们能装多少水,再取最大值。这个思路当然没问题,但时间复杂度太高,最终会发现这道题真正想考察的,其实是你对 双指针优化 的理解。
本文就结合你这份 Java 代码,系统地讲一下这道题的解法。
题目介绍
给定一个长度为 n 的整数数组 height。
数组中的每个元素代表一根竖线的高度,第 i 根竖线的两个端点分别是:
-
(i, 0) -
(i, height[i])
现在需要你从中选出两根竖线,与 x 轴一起构成一个容器。
这个容器所能盛水的面积由下面这个公式决定:
面积 = 宽度 × 高度 = (right - left) × min(height[left], height[right])
其中:
-
left和right是两根竖线的位置 -
宽度就是它们之间的距离
-
高度由两根竖线中较短的那一根决定,因为水位不可能超过短板
题目要求返回这个容器最多可以盛多少水。
示例
示例 1
输入:height = [1,8,6,2,5,4,8,3,7] 输出:49
为什么答案是 49?
因为选择下标 1 和下标 8 这两根柱子时:
-
宽度:
8 - 1 = 7 -
高度:
min(8, 7) = 7
所以面积为:
7 × 7 = 49
示例 2
输入:height = [1,1] 输出:1
只有两根柱子,宽度是 1,高度是 1,面积自然就是 1。
最直接的思路:暴力枚举
如果按照最朴素的想法,我们可以枚举所有的两根柱子:
-
第一根柱子位置是
i -
第二根柱子位置是
j -
计算面积
(j - i) * min(height[i], height[j]) -
最后取最大值
伪代码大概是这样:
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
ans = Math.max(ans, (j - i) * Math.min(height[i], height[j]));
}
}
这个方法当然能做出来,但时间复杂度是:
O(n^2)
当数组长度比较大时,这种做法效率就不够高了。
所以这道题更优的做法,是 双指针。
双指针解法
这道题最经典的思路就是:
-
用两个指针,一个放在最左边
left -
一个放在最右边
right -
每次先计算当前这两个位置形成的面积
-
然后移动较短的那一边
你给出的代码就是这个标准写法:
class Solution {
public int maxArea(int[] height) {
int maxArea = 0;
int left = 0;
int right = height.length - 1;
while (left < right) {
int currentArea = (right - left) * Math.min(height[left], height[right]);
maxArea = Math.max(maxArea, currentArea);
if (height[left] < height[right]) left++;
else right--;
}
return maxArea;
}
}
这段代码很短,但背后的思想非常重要。
为什么要从两端开始?
因为容器面积由两个因素决定:
-
两根柱子之间的距离
-
两根柱子中较短的高度
如果我们一开始把指针放在数组两端,那么这时候的宽度是最大的。
虽然当前的高度不一定理想,但至少宽度已经拉满了。之后我们再通过不断移动指针,尝试寻找更高的短板,看看能不能在宽度变小的同时,让高度提升更多,从而得到更大的面积。
所以“双指针从两端向中间逼近”,本质上是在做一种有方向的搜索,而不是盲目枚举。
为什么只移动较短的那一边?
这是整道题最关键的地方。
假设当前两根柱子分别是:
-
左边高度
height[left] -
右边高度
height[right]
当前面积是:
(right - left) * min(height[left], height[right])
如果此时:
height[left] < height[right]
那么当前容器的高度就取决于左边这根较短的柱子。
换句话说,左边这根柱子就是当前的“短板”。
这时候如果你去移动右指针,会发生什么?
-
宽度一定会变小
-
而高度上限仍然受左边短板限制
也就是说,面积大概率不会变得更优。因为决定当前容量上限的是短板,不解决短板问题,单纯移动长板没有意义。
相反,如果移动左指针,就有机会找到一根更高的柱子。虽然宽度变小了,但如果新的短板更高,面积仍然有可能增大。
这就是为什么要移动较短的一边。
这个结论可以简单记成一句话:
想让面积变大,只能尝试提升短板。
代码拆解
下面结合代码,一步一步来看。
1. 初始化左右指针
int left = 0;
int right = height.length - 1;
左指针放在最左边,右指针放在最右边。
这样一开始拿到的是最大的宽度。
2. 计算当前面积
int currentArea = (right - left) * Math.min(height[left], height[right]);
这个公式很好理解:
-
(right - left)表示宽度 -
Math.min(height[left], height[right])表示高度
因为水位只能到较短的那根柱子。
3. 更新最大值
maxArea = Math.max(maxArea, currentArea);
每算出一个面积,就和当前最大值比较,保留更大的那个。
4. 移动较短的一边
if (height[left] < height[right]) left++;
else right--;
如果左边更短,就移动左边;否则移动右边。
这里的 else 其实也包含了两边相等的情况。 当两边高度相等时,移动哪一边都可以,因为当前短板一样高,随便移动一边都不会漏掉最优解。
执行过程演示
我们用经典样例来手动模拟一下:
height = [1,8,6,2,5,4,8,3,7]
第一次
-
left = 0,高度1 -
right = 8,高度7
面积:
(8 - 0) * min(1, 7) = 8
当前最大面积是 8。
因为左边更短,所以移动左指针。
第二次
-
left = 1,高度8 -
right = 8,高度7
面积:
(8 - 1) * min(8, 7) = 7 * 7 = 49
当前最大面积更新为 49。
因为右边更短,所以移动右指针。
第三次
-
left = 1,高度8 -
right = 7,高度3
面积:
(7 - 1) * min(8, 3) = 6 * 3 = 18
比 49 小,不更新。
因为右边更短,继续移动右指针。
后面继续重复这个过程,最终最大值仍然是 49。
为什么双指针不会漏掉答案?
很多人第一次学这道题的时候,最担心的问题就是:
“我每次只移动一边,会不会错过某个更优解?”
其实不会。
原因就在于:
当两端确定以后,面积由短板决定。 如果你不移动短板,而去移动长板,那么:
-
宽度变小了
-
短板没变,甚至可能更低
这种情况下,不可能得到比当前更优的结果。
所以移动长板是没有意义的,可以直接舍弃。
换句话说,双指针并不是“拍脑袋乱试”,而是在每一步都合理排除掉一部分不可能成为最优解的情况。
这其实就是一种典型的 贪心思想。
复杂度分析
时间复杂度
O(n)
因为左右指针最多各移动 n 次,总共只会遍历数组一遍。
空间复杂度
O(1)
只用了几个额外变量,没有使用额外数组或集合。
这也是这道题双指针解法最大的优势:不仅快,而且非常省空间。
这道题真正想考什么?
表面上看,这是一道“求最大面积”的题。
但本质上,它想考的是你能不能从公式中提取出关键矛盾:
面积 = 宽度 × 短板高度
其中:
-
宽度随着指针移动一定会变小
-
所以如果想让面积有机会变大,就只能尽量提高短板高度
一旦你抓住这个核心,就能自然想到:
每次移动较短的那一边。
这类题目特别适合训练对“双指针 + 贪心”的理解。因为很多时候,最难的不是写代码,而是证明“为什么这么移动是对的”。
总结
这道题的代码其实很短,但思维含量很高。
它的核心可以概括成下面三点:
-
面积由宽度和短板共同决定
-
宽度在双指针收缩过程中一定会变小
-
所以想让面积变大,只能尝试移动短板,寻找更高的柱子
因此,最优解法就是:
-
-
每次计算当前面积
-
更新最大值
-
然后移动较短的一边
最终就能在 O(n) 的时间复杂度内解决问题。
如果你刚开始刷 LeetCode,这道题非常值得反复理解。 因为它不仅是一道高频面试题,也是一道非常典型的双指针启蒙题。










