LeetCode 11. 盛最多水的容器 - 双指针贪心解法详解
一、问题理解
问题描述
给定一个长度为 n 的整数数组 height,其中 height[i] 表示第 i 根柱子的高度。两根柱子与横轴组成一个"容器",容器的容量 = 两根柱子之间的距离(宽度) × 两根柱子中较矮的高度。
目标:找出数组中能容纳最多水的容器。
示例
text
输入: height = [1,8,6,2,5,4,8,3,7]
输出: 49
解释: 最大容量由索引 1(高度 8)和索引 8(高度 7)组成
宽度 = 8 - 1 = 7,高度 = min(8, 7) = 7
容量 = 7 × 7 = 49
二、核心思路:双指针 + 贪心策略
暴力解法 vs 双指针法
-
暴力解法:遍历所有可能的柱子对
(i, j),计算每个容器的容量,时间复杂度为 O(n²) -
双指针法:通过贪心策略缩小搜索范围,时间复杂度优化到 O(n)
双指针贪心策略
-
双指针初始化:
-
左指针
left从数组开头(索引 0)开始 -
右指针
right从数组末尾(索引 n-1)开始 -
此时宽度最大,提供了初始的最大潜在容量
-
-
计算容量公式:
text
容量 = 宽度 × 高度 = (right - left) × min(height[left], height[right]) -
贪心移动策略:
-
每次移动较矮的柱子指针
-
为什么移动较矮的柱子?
-
如果移动较高的柱子:宽度减小,而高度最多还是由原来的矮柱决定,容量必然变小
-
如果移动较矮的柱子:宽度减小,但可能遇到更高的柱子,从而可能增大容量
-
-
三、代码逐行解析
Java 解法
java
class Solution {
public int maxArea(int[] height) {
// 1. 初始化左指针(数组开头)
int left = 0;
// 2. 初始化右指针(数组末尾)
int right = height.length - 1;
// 3. 存储最大容量(初始为0)
int maxArea = 0;
// 4. 双指针循环:当左指针在右指针左侧时,继续缩小范围
while (left < right) {
// 5. 计算当前容器的宽度
int width = right - left;
// 6. 计算当前容器的高度(取较矮柱子)
int minHeight = Math.min(height[left], height[right]);
// 7. 计算当前容量
int currentArea = width * minHeight;
// 8. 更新最大容量
maxArea = Math.max(maxArea, currentArea);
// 9. 贪心移动指针:移动较矮的柱子
if (height[left] < height[right]) {
left++; // 左柱较矮,左指针右移
} else {
right--; // 右柱较矮(或等高),右指针左移
}
}
// 10. 返回最大容量
return maxArea;
}
}
Python 解法
python
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
# 1. 初始化双指针
left, right = 0, len(height) - 1
# 2. 初始化最大容量
max_area = 0
# 3. 双指针循环
while left < right:
# 4. 计算当前宽度和高度
width = right - left
min_height = min(height[left], height[right])
# 5. 计算当前容量并更新最大值
current_area = width * min_height
max_area = max(max_area, current_area)
# 6. 贪心移动指针:移动较矮的柱子
if height[left] < height[right]:
left += 1 # 左柱较矮,左指针右移
else:
right -= 1 # 右柱较矮(或等高),右指针左移
# 7. 返回最大容量
return max_area
四、Java 与 Python 语法对比
1. 变量声明与初始化
| 操作 | Java | Python |
|---|---|---|
| 变量声明 | int left = 0; | left = 0 |
| 多变量声明 | int left = 0, right = n-1; | left, right = 0, n-1 |
| 数组/列表长度 | height.length | len(height) |
2. 循环与控制流
| 操作 | Java | Python |
|---|---|---|
| while 循环 | while (left < right) { ... } | while left < right: |
| if-else 语句 | if (condition) { ... } else { ... } | if condition: ... else: ... |
| 递增/递减 | left++ / right-- | left += 1 / right -= 1 |
3. 数学运算与函数
| 操作 | Java | Python |
|---|---|---|
| 最小值 | Math.min(a, b) | min(a, b) |
| 最大值 | Math.max(a, b) | max(a, b) |
| 绝对值 | Math.abs(num) | abs(num) |
五、实例演示
以测试用例 height = [1, 8, 6, 2, 5, 4, 8, 3, 7] 为例,演示双指针移动过程:
| 步骤 | left | right | 宽度 | 高度 | 当前容量 | 最大容量 | 移动指针(原因) |
|---|---|---|---|---|---|---|---|
| 初始 | 0 | 8 | 8 | min(1,7)=1 | 8 | 8 | left++ (左柱矮) |
| 1 | 1 | 8 | 7 | min(8,7)=7 | 49 | 49 | right-- (右柱矮) |
| 2 | 1 | 7 | 6 | min(8,3)=3 | 18 | 49 | right-- (右柱矮) |
| 3 | 1 | 6 | 5 | min(8,8)=8 | 40 | 49 | right-- (等高,移动任意) |
| 4 | 1 | 5 | 4 | min(8,4)=4 | 16 | 49 | right-- (右柱矮) |
| 5 | 1 | 4 | 3 | min(8,5)=5 | 15 | 49 | right-- (右柱矮) |
| 6 | 1 | 3 | 2 | min(8,2)=2 | 4 | 49 | right-- (右柱矮) |
| 7 | 1 | 2 | 1 | min(8,6)=6 | 6 | 49 | right-- (右柱矮) |
| 结束 | 1 | 1 | - | - | - | 49 | left < right 不成立 |
最终结果:最大容量为 49
六、关键细节与复杂度分析
1. 为什么移动较矮的柱子是正确的?
假设当前左柱高度 < 右柱高度:
-
如果移动右柱(较高的柱子):宽度减小,新高度 ≤ 左柱高度(因为左柱更矮),所以新容量必然小于当前容量
-
如果移动左柱(较矮的柱子):宽度减小,但可能遇到更高的左柱,新高度可能增加,从而可能得到更大的容量
贪心选择证明:移动较矮的柱子不会错过最大容量解,因为任何包含当前矮柱的更大容量的容器都不可能存在(与当前高柱的组合已经计算过,与其他柱子的宽度更小,高度不会超过当前矮柱)。
2. 当两柱高度相等时,移动哪一侧?
代码中当 height[left] >= height[right] 时移动右指针,但实际上移动左指针也可以。因为两柱等高时:
-
移动任何一侧,宽度都减小
-
新高度最多等于当前高度(可能更小)
-
因此新容量不会超过当前容量
关键点:必须移动指针才能继续循环,但移动哪一侧不影响最终结果。
3. 复杂度分析
-
时间复杂度:O(n)
-
每个指针从两端向中间移动,每个元素最多被访问一次
-
总操作次数为 n-1
-
-
空间复杂度:O(1)
-
只使用了常数个额外变量(left, right, maxArea, currentArea等)
-
七、优化技巧与变体
1. 更简洁的 Python 写法
python
class Solution:
def maxArea(self, height: List[int]) -> int:
left, right = 0, len(height) - 1
max_area = 0
while left < right:
# 直接计算并更新
max_area = max(max_area, (right - left) * min(height[left], height[right]))
# 移动较矮的柱子
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
2. 处理边界情况
python
class Solution:
def maxArea(self, height: List[int]) -> int:
# 处理空数组或单元素数组
if len(height) < 2:
return 0
left, right = 0, len(height) - 1
max_area = 0
while left < right:
# 计算当前容量
width = right - left
min_height = min(height[left], height[right])
current_area = width * min_height
# 更新最大容量
if current_area > max_area:
max_area = current_area
# 贪心移动指针
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
3. 可视化理解(辅助代码)
python
def maxArea_visual(height):
left, right = 0, len(height) - 1
max_area = 0
step = 0
print("初始数组:", height)
print("索引: ", list(range(len(height))))
while left < right:
step += 1
width = right - left
min_height = min(height[left], height[right])
current_area = width * min_height
max_area = max(max_area, current_area)
print(f"\n步骤 {step}:")
print(f" left={left}, right={right}")
print(f" 高度: height[{left}]={height[left]}, height[{right}]={height[right]}")
print(f" 宽度: {right} - {left} = {width}")
print(f" 容量: {width} × {min_height} = {current_area}")
print(f" 最大容量: {max_area}")
if height[left] < height[right]:
print(f" 移动左指针 (height[{left}]={height[left]} < height[{right}]={height[right]})")
left += 1
else:
print(f" 移动右指针 (height[{left}]={height[left]} >= height[{right}]={height[right]})")
right -= 1
return max_area
# 测试
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
print("最终结果:", maxArea_visual(height))
八、常用函数积累
Java 常用函数
java
// 数组操作
int[] arr = {1, 2, 3};
int length = arr.length; // 数组长度
// 数学运算
Math.min(a, b); // 返回最小值
Math.max(a, b); // 返回最大值
Math.abs(a); // 返回绝对值
// 循环与控制
for (int i = 0; i < n; i++) { } // for循环
while (condition) { } // while循环
if (condition) { } else { } // if-else语句
Python 常用函数
python
# 列表操作 lst = [1, 2, 3] length = len(lst) # 列表长度 # 内置函数 min(a, b) # 返回最小值 max(a, b) # 返回最大值 abs(a) # 返回绝对值 # 循环与控制 for i in range(n): # for循环 while condition: # while循环 if condition: # if-else语句
九、总结
核心要点
-
双指针是核心:从数组两端开始,向中间移动
-
贪心策略是关键:总是移动较矮的柱子,以寻求更大的容量
-
时间复杂度优化:从暴力解法的 O(n²) 优化到 O(n)
面试常见问题
-
为什么这个解法是正确的?
-
通过移动较矮的柱子,我们保留了找到更大容量的可能性,而不会错过最优解
-
-
当两柱高度相等时,移动哪一侧?
-
移动任意一侧都可以,因为新容量不会超过当前容量
-
-
这个算法是否会错过最大容量?
-
不会。算法的贪心选择保证了每次移动都是当前最优的,最终会找到全局最优解
-
-
如何处理空数组或单元素数组?
-
空数组或单元素数组无法构成容器,应返回 0
-
-
这个算法适用于负数高度吗?
-
题目假设高度为非负整数,但算法本身也适用于负数(不过实际意义不大)
-
扩展思考
-
类似问题:这种双指针从两端向中间移动的模式也适用于其他问题,如:
-
两数之和 II(输入数组已排序)
-
接雨水问题(需要更复杂的逻辑)
-
验证回文串
-
-
变体问题:如果要求找到三个柱子形成的最大容量容器(三维容器),该如何解决?
-
这需要更复杂的算法,可能涉及排序和双指针的组合
-
-
实际应用:盛水容器问题在计算几何和实际工程中都有应用,如:
-
水库容量计算
-
建筑设计中雨水收集系统设计
-
图像处理中的区域检测
-
掌握双指针+贪心的解法思想,不仅能够解决这道LeetCode问题,还能够应用到其他许多需要高效搜索的问题中,是面试中非常重要的算法技巧。


被折叠的 条评论
为什么被折叠?



