- Remove all "### Java 实现" sections from all markdown files - Clean up remaining Java code blocks and references - Ensure only Go code remains in all documentation - Complete the Java to Go migration for all 22 files
9.5 KiB
盛最多水的容器 (Container With Most Water)
题目描述
给定一个长度为 n 的整数数组 height。有 n 条垂直线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。
找出两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
**说明:**你不能倾斜容器。
示例
示例 1:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例 2:
输入:[1,1]
输出:1
约束条件
n == height.length2 <= n <= 10^50 <= height[i] <= 10^4
解题思路
方法一:双指针法(最优解)
**核心思想:**使用两个指针,一个在数组开头,一个在数组末尾。每次移动较短的指针向中间靠拢。
为什么这样做?
- 容器的容量由
min(height[left], height[right]) * (right - left)决定 - 如果移动较高的指针,宽度减小,高度只能保持不变或减小,容量一定不会增大
- 如果移动较短的指针,虽然宽度减小,但可能会找到更高的线,从而增大容量
算法步骤:
- 初始化
left = 0,right = len(height) - 1,maxArea = 0 - 当
left < right时:- 计算当前面积:
area = min(height[left], height[right]) * (right - left) - 更新
maxArea = max(maxArea, area) - 如果
height[left] < height[right],则left++,否则right--
- 计算当前面积:
- 返回
maxArea
方法二:暴力枚举(不推荐)
枚举所有可能的线对,计算每对线构成的容器容量,取最大值。时间复杂度 O(n²),会超时。
代码实现
Go 实现
package main
import (
"fmt"
)
func maxArea(height []int) int {
left, right := 0, len(height)-1
maxArea := 0
for left < right {
// 计算当前面积
width := right - left
h := height[left]
if height[right] < h {
h = height[right]
}
area := width * h
// 更新最大面积
if area > maxArea {
maxArea = area
}
// 移动较短的指针
if height[left] < height[right] {
left++
} else {
right--
}
}
return maxArea
}
// 测试用例
func main() {
// 测试用例1
height1 := []int{1, 8, 6, 2, 5, 4, 8, 3, 7}
fmt.Printf("输入: %v\n", height1)
fmt.Printf("输出: %d\n", maxArea(height1)) // 期望输出: 49
// 测试用例2
height2 := []int{1, 1}
fmt.Printf("\n输入: %v\n", height2)
fmt.Printf("输出: %d\n", maxArea(height2)) // 期望输出: 1
// 测试用例3: 递增序列
height3 := []int{1, 2, 3, 4, 5}
fmt.Printf("\n输入: %v\n", height3)
fmt.Printf("输出: %d\n", maxArea(height3)) // 期望输出: 6
// 测试用例4: 递减序列
height4 := []int{5, 4, 3, 2, 1}
fmt.Printf("\n输入: %v\n", height4)
fmt.Printf("输出: %d\n", maxArea(height4)) // 期望输出: 6
// 测试用例5: 包含0
height5 := []int{0, 2}
fmt.Printf("\n输入: %v\n", height5)
fmt.Printf("输出: %d\n", maxArea(height5)) // 期望输出: 0
}
-
时间复杂度: O(n)
- 只需遍历数组一次,每次移动一个指针
- 指针最多移动 n 次
-
空间复杂度: O(1)
- 只使用了常数级别的额外空间
- 只需要几个变量存储指针和最大面积
暴力枚举
-
时间复杂度: O(n²)
- 需要枚举所有可能的线对
- 共有 n(n-1)/2 种组合
-
空间复杂度: O(1)
- 只需要常数级别的额外空间
进阶问题
Q1: 如果可以倾斜容器,问题会如何变化?
A: 如果可以倾斜容器,问题会变得复杂得多。需要考虑水的倾斜角度和容器的几何形状,这涉及到更多的物理和几何计算。
Q2: 如果需要返回构成最大容器的两条线的索引,应该如何修改?
A: 在更新 maxArea 的同时,记录当前的 left 和 right 索引。
// Go 版本
func maxAreaWithIndex(height []int) (int, int, int) {
left, right := 0, len(height)-1
maxArea, bestL, bestR := 0, 0, 0
for left < right {
h := height[left]
if height[right] < h {
h = height[right]
}
area := (right - left) * h
if area > maxArea {
maxArea = area
bestL, bestR = left, right
}
if height[left] < height[right] {
left++
} else {
right--
}
}
return maxArea, bestL, bestR
}
Q3: 如果数组中有负数,应该如何处理?
A: 如果高度可以为负数,需要先过滤掉负数或取绝对值。通常物理意义上的高度不应为负,但如果题目允许,可以这样处理:
// 处理负数:取绝对值
func maxAreaWithNegative(height []int) int {
left, right := 0, len(height)-1
maxArea := 0
for left < right {
h := height[left]
if height[right] < h {
h = height[right]
}
h = abs(h) // 取绝对值
area := (right - left) * h
maxArea = max(maxArea, area)
if abs(height[left]) < abs(height[right]) {
left++
} else {
right--
}
}
return maxArea
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
P7 加分项
1. 深度理解:为什么双指针法一定正确?
数学证明:
假设当前指针在 left 和 right,且 height[left] < height[right]。
我们要证明:移动 right 指针一定不会得到更大的面积。
- 当前面积:
S1 = height[left] * (right - left) - 移动
right后的面积:S2 = min(height[left], height[right-1]) * (right - 1 - left) - 由于
height[left] < height[right],且right-1 < right - 所以
S2 <= height[left] * (right - 1 - left) < height[left] * (right - left) = S1
因此,移动较高的指针不会得到更大的面积。
2. 实战扩展:接雨水问题 (Trapping Rain Water)
LeetCode 42: 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
func trap(height []int) int {
if len(height) < 3 {
return 0
}
left, right := 0, len(height)-1
leftMax, rightMax := 0, 0
water := 0
for left < right {
if height[left] < height[right] {
if height[left] >= leftMax {
leftMax = height[left]
} else {
water += leftMax - height[left]
}
left++
} else {
if height[right] >= rightMax {
rightMax = height[right]
} else {
water += rightMax - height[right]
}
right--
}
}
return water
}
核心区别:
- 盛水容器:找两条线构成最大面积
- 接雨水:计算所有能接的雨水总量
3. 变形题目
变形1:盛最多水的容器 II(允许倾斜)
如果允许容器倾斜,最大水量取决于两条线之间的最小距离和角度。
变形2:三维盛水
给定一个 m × n 的矩阵,每个格子表示高度,找出能盛最多水的四个角构成的容器。
变形3:动态盛水
容器的高度会随时间变化,求某个时间段内能盛的最大水量。
4. 优化技巧
优化1:提前终止
如果当前可能的面积(即使宽度最大)已经小于 maxArea,可以提前终止。
func maxAreaOptimized(height []int) int {
left, right := 0, len(height)-1
maxArea := 0
for left < right {
area := (right - left) * min(height[left], height[right])
maxArea = max(maxArea, area)
// 提前终止:如果当前宽度已经很窄,可能无法超过maxArea
if right-left <= maxArea/max(height[left], height[right]) {
break
}
if height[left] < height[right] {
left++
} else {
right--
}
}
return maxArea
}
优化2:跳过明显不可能的线
如果移动后的线高度比移动前还低,可以继续移动直到找到更高的线。
func maxAreaSkip(height []int) int {
left, right := 0, len(height)-1
maxArea := 0
for left < right {
area := (right - left) * min(height[left], height[right])
maxArea = max(maxArea, area)
if height[left] < height[right] {
oldLeft := left
left++
// 跳过比oldLeft还低的线
for left < right && height[left] <= height[oldLeft] {
left++
}
} else {
oldRight := right
right--
// 跳过比oldRight还低的线
for left < right && height[right] <= height[oldRight] {
right--
}
}
}
return maxArea
}
5. 实际应用场景
- 水库设计: 计算水库的最大蓄水量
- 城市规划: 确定建筑物之间的最佳距离以最大化绿化面积
- 数据压缩: 在某些压缩算法中寻找最优的分段点
6. 面试技巧
面试官可能会问:
- "为什么双指针法一定能找到最优解?"
- "如果数组有 10^8 个元素,你的算法还能用吗?"
- "如何证明你的算法是正确的?"
回答要点:
- 给出数学证明(如上所述)
- 讨论算法的局限性,考虑分布式处理
- 提供正确的证明思路
7. 相关题目推荐
- LeetCode 42: 接雨水
- LeetCode 11: 盛最多水的容器(本题)
- LeetCode 84: 柱状图中最大的矩形
- LeetCode 85: 最大矩形