目录
- 1.问题描述
- 2.问题分析
-
- [2.1 理解题目](#2.1 理解题目)
- [2.2 关键挑战](#2.2 关键挑战)
- [2.3 核心洞察](#2.3 核心洞察)
- [2.4 破题关键](#2.4 破题关键)
- 3.算法设计与实现
-
- [3.1 双指针法(最优解)](#3.1 双指针法(最优解))
- [4.2 暴力枚举法](#4.2 暴力枚举法)
- 4.性能分析与对比
-
- [4.1 复杂度对比](#4.1 复杂度对比)
- [4.2 性能分析](#4.2 性能分析)
- [4.3 正确性证明补充](#4.3 正确性证明补充)
- 5.边界情况处理
-
- [5.1 常见边界情况](#5.1 常见边界情况)
- [5.2 边界测试用例](#5.2 边界测试用例)
- 6.扩展与变体
-
- [6.1 三维容器问题](#6.1 三维容器问题)
- [6.2 容器带隔板问题](#6.2 容器带隔板问题)
- [6.3 寻找第二大容量](#6.3 寻找第二大容量)
- [6.4 容器容量实时更新](#6.4 容器容量实时更新)
- [6.5 容器形状变化(可倾斜)](#6.5 容器形状变化(可倾斜))
- 7.总结
-
- [7.1 核心知识点总结](#7.1 核心知识点总结)
- [7.2 算法思维提升](#7.2 算法思维提升)
- [7.3 实际应用场景](#7.3 实际应用场景)
1.问题描述
给定一个长度为 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
输入:height = [1,1]
输出:1
约束条件
n == height.length2 <= n <= 10^50 <= height[i] <= 10^4
2.问题分析
2.1 理解题目
本题要求在一个表示垂线高度的数组中,找到两条垂线,使得它们与x轴构成的容器能够容纳最多的水。关键要点:
- 容器形状:由两条垂线和x轴围成的矩形区域
- 容器高度:由两条垂线中较短的那条决定(木桶原理)
- 容器宽度:两条垂线之间的水平距离
- 水量计算 :面积 = 最小高度 × 宽度 =
min(height[i], height[j]) × (j - i) - 目标:在所有可能的(i, j)组合中,找到面积最大的
2.2 关键挑战
- 数据规模大:数组长度最大为10^5,需要高效算法
- 暴力法不可行:O(n²)的暴力枚举会超时
- 最优解证明:需要理解为什么双指针法是正确的
- 边界情况:需要考虑数组长度为2、所有高度相同等特殊情况
2.3 核心洞察
- 面积公式:面积由两个因素决定:宽度和最小高度
- 权衡关系:宽度和高度之间存在权衡 - 要获得更大宽度,需要选择更远的线,但可能牺牲高度;要获得更大高度,需要选择更高的线,但可能牺牲宽度
- 双指针移动原理:从最宽的容器开始(左右端点),然后逐步缩小宽度,但尝试增加高度
- 贪心选择:总是移动较短的边,因为移动较长的边不可能得到更大的面积
2.4 破题关键
- 初始状态:从最宽的容器开始(左指针在0,右指针在n-1)
- 移动策略:比较左右指针的高度,移动较短的指针
- 正确性证明 :
- 当前容器的面积受限于较短的边
- 如果移动较长的边:宽度减小,高度不会超过原来的短边,所以面积一定减小
- 如果移动较短的边:宽度减小,但可能遇到更高的边,从而可能增加面积
- 终止条件:当左右指针相遇时停止
3.算法设计与实现
3.1 双指针法(最优解)
核心思想
从数组的两端开始,使用两个指针向中间移动。每次计算当前指针所表示容器的面积,并更新最大面积。移动高度较小的指针,因为这样有可能找到更高的边从而获得更大的面积。
算法思路
- 初始化 :设置左指针
left = 0,右指针right = n-1,最大面积maxArea = 0 - 循环条件 :当
left < right时继续循环 - 计算当前面积 :
- 高度:
min(height[left], height[right]) - 宽度:
right - left - 面积:
area = min(height[left], height[right]) × (right - left)
- 高度:
- 更新最大面积 :
maxArea = max(maxArea, area) - 移动指针 :
- 如果
height[left] < height[right],则left++ - 否则
right--
- 如果
- 返回结果 :循环结束后返回
maxArea
正确性证明
关键问题:为什么移动较短的边是正确的?
假设当前左右指针分别为 i 和 j,且 height[i] < height[j]:
- 如果移动较长的边
j到j-1:- 宽度减少:
(j-1) - i < j - i - 新容器的高度:
min(height[i], height[j-1]) ≤ height[i](因为高度受限于较短边) - 所以新面积 ≤ 原面积
- 宽度减少:
- 如果移动较短的边
i到i+1:- 宽度减少
- 但新容器的高度可能大于
height[i],从而可能得到更大的面积
因此,移动较短的边是寻找更大面积的唯一希望。
时间复杂度分析
- 每个元素最多被访问一次:O(n)
- 每次迭代执行常数时间操作
- 总时间复杂度:O(n)
空间复杂度分析
- 只使用了常数级别的额外空间:O(1)
代码实现
java
public class ContainerWithMostWater {
public int maxArea(int[] height) {
// 边界条件检查
if (height == null || height.length < 2) {
return 0;
}
int left = 0; // 左指针
int right = height.length - 1; // 右指针
int maxArea = 0; // 最大面积
// 双指针向中间移动
while (left < right) {
// 计算当前容器的面积
int currentHeight = Math.min(height[left], height[right]);
int width = right - left;
int area = currentHeight * width;
// 更新最大面积
maxArea = Math.max(maxArea, area);
// 移动较短的边
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
}
优化版本(减少方法调用)
java
public class ContainerWithMostWaterOptimized {
public int maxArea(int[] height) {
if (height == null || height.length < 2) return 0;
int maxArea = 0;
int left = 0;
int right = height.length - 1;
while (left < right) {
// 直接计算,避免多次调用Math.min
int h = height[left] < height[right] ? height[left] : height[right];
int area = h * (right - left);
maxArea = area > maxArea ? area : maxArea;
// 移动指针
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
}
4.2 暴力枚举法
核心思想
枚举所有可能的线对组合,计算每个容器的面积,记录最大值。这种方法简单直观,但时间复杂度高,不适用于大规模数据。
算法思路
- 双重循环:外层循环遍历所有可能的左边界,内层循环遍历所有可能的右边界
- 计算面积 :对于每对(i, j),计算面积
min(height[i], height[j]) × (j - i) - 更新最大值:记录遇到的最大面积
- 返回结果:遍历完成后返回最大面积
时间复杂度分析
- 双重循环:O(n²)
- 当n=105时,操作次数约为1010,不可接受
空间复杂度分析
- 只使用了常数级别的额外空间:O(1)
代码实现
java
public class ContainerWithMostWaterBruteForce {
public int maxArea(int[] height) {
if (height == null || height.length < 2) {
return 0;
}
int maxArea = 0;
int n = height.length;
// 枚举所有可能的线对
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 计算当前容器的面积
int h = Math.min(height[i], height[j]);
int width = j - i;
int area = h * width;
// 更新最大面积
if (area > maxArea) {
maxArea = area;
}
}
}
return maxArea;
}
}
4.性能分析与对比
4.1 复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 实现难度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 双指针法 | O(n) | O(1) | ★★☆☆☆ | 效率高,代码简洁 | 正确性需要理解 |
| 暴力枚举法 | O(n²) | O(1) | ★☆☆☆☆ | 思路简单,易于实现 | 效率低,不可用于大数据 |
4.2 性能分析
-
时间效率:
- 双指针法:对于n=105,只需约105次操作
- 暴力法:对于n=105,需要约5×109次操作,相差5万倍
-
空间效率:所有方法都只需要常数空间
-
实际性能:在LeetCode测试中,双指针法通常在2-3ms内完成,而暴力法会超时
4.3 正确性证明补充
双指针法的正确性可以通过反证法证明:
假设最优解是(i*, j*),我们需要证明双指针法能找到这个解。
在双指针移动过程中:
- 初始时,
left=0,right=n-1 - 如果
height[0] < height[n-1],则移动left - 关键点:当
left移动到i*时,right一定还在j*的右边(因为如果right已经移到j*左边,那么已经错过了最优解) - 类似地,可以证明双指针一定会经过最优解对应的状态
更形式化的证明需要数学归纳法,但直观理解足够:我们总是保留可能产生更大面积的边。
5.边界情况处理
5.1 常见边界情况
- 数组长度小于2:无法构成容器,返回0
- 数组长度为2:直接计算这两个元素构成的容器面积
- 所有高度相同:最大面积由最宽容器决定,即第一个和最后一个元素
- 高度为0:不影响算法,因为高度为0的容器面积为0
- 递增序列:最大面积由第一个和最后一个元素决定
- 递减序列:同样由第一个和最后一个元素决定
5.2 边界测试用例
java
// 测试各种边界情况
int[][] testCases = {
{}, // 空数组
{1}, // 单个元素
{1, 1}, // 两个相同元素
{1, 2, 1}, // 先增后减
{1, 2, 3, 4, 5}, // 递增序列
{5, 4, 3, 2, 1}, // 递减序列
{0, 0, 0, 0}, // 全零
{1, 8, 6, 2, 5, 4, 8, 3, 7}, // 示例
{2, 3, 4, 5, 18, 17, 6}, // 复杂案例
};
6.扩展与变体
6.1 三维容器问题
在三维空间中寻找最大容积,类似问题但更复杂。
java
// 三维版本,需要更多约束条件
// 这只是一个概念性的扩展
public class Container3D {
// 三维情况更复杂,可能需要在二维平面上寻找最大面积
// 或者考虑长方体容器
}
6.2 容器带隔板问题
容器中间有隔板,需要计算多个区域的总容量。
java
public class ContainerWithPartitions {
/**
* 计算带隔板的容器最大容量
* 隔板高度已知,需要计算每个隔间的容量
*/
public int totalAreaWithPartitions(int[] height, int[] partitions) {
// 思路:将容器分成多个子区间,分别计算每个子区间的最大容量
int total = 0;
int prev = 0;
for (int partition : partitions) {
// 提取子数组
int[] subArray = Arrays.copyOfRange(height, prev, partition + 1);
total += new ContainerWithMostWater().maxArea(subArray);
prev = partition;
}
// 最后一段
int[] lastSubArray = Arrays.copyOfRange(height, prev, height.length);
total += new ContainerWithMostWater().maxArea(lastSubArray);
return total;
}
}
6.3 寻找第二大容量
在找到最大容量后,寻找第二大的容量。
java
public class SecondLargestContainer {
public int secondMaxArea(int[] height) {
if (height == null || height.length < 3) {
return 0; // 至少需要3条线才有第二大
}
int max1 = 0; // 最大面积
int max2 = 0; // 第二大面积
int left = 0;
int right = height.length - 1;
while (left < right) {
int h = Math.min(height[left], height[right]);
int area = h * (right - left);
// 更新最大和第二大
if (area > max1) {
max2 = max1;
max1 = area;
} else if (area > max2 && area < max1) {
max2 = area;
}
// 移动指针
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return max2;
}
}
6.4 容器容量实时更新
支持动态添加新的垂线,并实时更新最大容量。
java
public class DynamicContainer {
private List<Integer> heights = new ArrayList<>();
private int maxArea = 0;
public void addHeight(int h) {
heights.add(h);
updateMaxArea();
}
private void updateMaxArea() {
// 重新计算最大面积
// 简单实现:使用双指针法重新计算
int[] arr = heights.stream().mapToInt(i -> i).toArray();
maxArea = new ContainerWithMostWater().maxArea(arr);
}
public int getMaxArea() {
return maxArea;
}
// 优化版本:增量更新,避免完全重新计算
public void addHeightOptimized(int h) {
heights.add(h);
int n = heights.size();
// 只检查新添加的线与原有线的组合
for (int i = 0; i < n - 1; i++) {
int area = Math.min(heights.get(i), h) * (n - 1 - i);
maxArea = Math.max(maxArea, area);
}
}
}
6.5 容器形状变化(可倾斜)
如果可以倾斜容器,问题变为寻找能容纳最多水的三角形或梯形。
java
public class TiltableContainer {
/**
* 如果容器可以倾斜,那么最大容量可能会增加
* 这变成了一个几何优化问题
*/
public double maxAreaTiltable(int[] height) {
// 简化模型:假设容器倾斜后,水的高度是两条边高度的平均值
// 这是一个不同的物理问题
double maxArea = 0;
int n = height.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 倾斜容器的容量公式不同
double avgHeight = (height[i] + height[j]) / 2.0;
double area = avgHeight * (j - i);
maxArea = Math.max(maxArea, area);
}
}
return maxArea;
}
}
7.总结
7.1 核心知识点总结
- 双指针技巧:从两端向中间移动,每次移动较短的边
- 贪心选择:总是移动较短的边,因为这是可能增加面积的唯一方式
- 面积计算 :
min(height[left], height[right]) × (right - left) - 算法正确性:通过反证法可以证明双指针法能找到最优解
7.2 算法思维提升
- 问题转化能力:将几何问题转化为数组操作问题
- 双指针应用:掌握在有序或部分有序情况下使用双指针
- 贪心策略设计:理解为什么移动较短边是最优选择
- 证明能力:学会证明算法正确性的基本方法
7.3 实际应用场景
- 水库设计:确定大坝的最佳位置以最大化蓄水量
- 城市规划:建筑物之间的空间利用最大化
- 货运优化:集装箱或货车的装载优化
- 数据分析:在时间序列中寻找最佳区间
面试建议
- 首选解法:双指针法,需要详细解释正确性
- 解题步骤 :
- 明确问题:容器面积由宽度和最小高度决定
- 提出双指针思路:从最宽容器开始
- 解释移动策略:为什么移动较短边
- 证明正确性:使用反证法或直观解释
- 分析复杂度:时间O(n),空间O(1)