LeetCode算法题详解 11:盛最多水的容器

目录

  • 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.length
  • 2 <= n <= 10^5
  • 0 <= height[i] <= 10^4

2.问题分析

2.1 理解题目

本题要求在一个表示垂线高度的数组中,找到两条垂线,使得它们与x轴构成的容器能够容纳最多的水。关键要点:

  1. 容器形状:由两条垂线和x轴围成的矩形区域
  2. 容器高度:由两条垂线中较短的那条决定(木桶原理)
  3. 容器宽度:两条垂线之间的水平距离
  4. 水量计算 :面积 = 最小高度 × 宽度 = min(height[i], height[j]) × (j - i)
  5. 目标:在所有可能的(i, j)组合中,找到面积最大的

2.2 关键挑战

  1. 数据规模大:数组长度最大为10^5,需要高效算法
  2. 暴力法不可行:O(n²)的暴力枚举会超时
  3. 最优解证明:需要理解为什么双指针法是正确的
  4. 边界情况:需要考虑数组长度为2、所有高度相同等特殊情况

2.3 核心洞察

  1. 面积公式:面积由两个因素决定:宽度和最小高度
  2. 权衡关系:宽度和高度之间存在权衡 - 要获得更大宽度,需要选择更远的线,但可能牺牲高度;要获得更大高度,需要选择更高的线,但可能牺牲宽度
  3. 双指针移动原理:从最宽的容器开始(左右端点),然后逐步缩小宽度,但尝试增加高度
  4. 贪心选择:总是移动较短的边,因为移动较长的边不可能得到更大的面积

2.4 破题关键

  1. 初始状态:从最宽的容器开始(左指针在0,右指针在n-1)
  2. 移动策略:比较左右指针的高度,移动较短的指针
  3. 正确性证明
    • 当前容器的面积受限于较短的边
    • 如果移动较长的边:宽度减小,高度不会超过原来的短边,所以面积一定减小
    • 如果移动较短的边:宽度减小,但可能遇到更高的边,从而可能增加面积
  4. 终止条件:当左右指针相遇时停止

3.算法设计与实现

3.1 双指针法(最优解)

核心思想

从数组的两端开始,使用两个指针向中间移动。每次计算当前指针所表示容器的面积,并更新最大面积。移动高度较小的指针,因为这样有可能找到更高的边从而获得更大的面积。

算法思路

  1. 初始化 :设置左指针 left = 0,右指针 right = n-1,最大面积 maxArea = 0
  2. 循环条件 :当 left < right 时继续循环
  3. 计算当前面积
    • 高度:min(height[left], height[right])
    • 宽度:right - left
    • 面积:area = min(height[left], height[right]) × (right - left)
  4. 更新最大面积maxArea = max(maxArea, area)
  5. 移动指针
    • 如果 height[left] < height[right],则 left++
    • 否则 right--
  6. 返回结果 :循环结束后返回 maxArea

正确性证明

关键问题:为什么移动较短的边是正确的?

假设当前左右指针分别为 ij,且 height[i] < height[j]

  • 如果移动较长的边 jj-1
    • 宽度减少:(j-1) - i < j - i
    • 新容器的高度:min(height[i], height[j-1]) ≤ height[i](因为高度受限于较短边)
    • 所以新面积 ≤ 原面积
  • 如果移动较短的边 ii+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 暴力枚举法

核心思想

枚举所有可能的线对组合,计算每个容器的面积,记录最大值。这种方法简单直观,但时间复杂度高,不适用于大规模数据。

算法思路

  1. 双重循环:外层循环遍历所有可能的左边界,内层循环遍历所有可能的右边界
  2. 计算面积 :对于每对(i, j),计算面积 min(height[i], height[j]) × (j - i)
  3. 更新最大值:记录遇到的最大面积
  4. 返回结果:遍历完成后返回最大面积

时间复杂度分析

  • 双重循环: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 性能分析

  1. 时间效率

    • 双指针法:对于n=105,只需约105次操作
    • 暴力法:对于n=105,需要约5×109次操作,相差5万倍
  2. 空间效率:所有方法都只需要常数空间

  3. 实际性能:在LeetCode测试中,双指针法通常在2-3ms内完成,而暴力法会超时

4.3 正确性证明补充

双指针法的正确性可以通过反证法证明:

假设最优解是(i*, j*),我们需要证明双指针法能找到这个解。

在双指针移动过程中:

  1. 初始时,left=0, right=n-1
  2. 如果height[0] < height[n-1],则移动left
  3. 关键点:当left移动到i*时,right一定还在j*的右边(因为如果right已经移到j*左边,那么已经错过了最优解)
  4. 类似地,可以证明双指针一定会经过最优解对应的状态

更形式化的证明需要数学归纳法,但直观理解足够:我们总是保留可能产生更大面积的边。

5.边界情况处理

5.1 常见边界情况

  1. 数组长度小于2:无法构成容器,返回0
  2. 数组长度为2:直接计算这两个元素构成的容器面积
  3. 所有高度相同:最大面积由最宽容器决定,即第一个和最后一个元素
  4. 高度为0:不影响算法,因为高度为0的容器面积为0
  5. 递增序列:最大面积由第一个和最后一个元素决定
  6. 递减序列:同样由第一个和最后一个元素决定

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 核心知识点总结

  1. 双指针技巧:从两端向中间移动,每次移动较短的边
  2. 贪心选择:总是移动较短的边,因为这是可能增加面积的唯一方式
  3. 面积计算min(height[left], height[right]) × (right - left)
  4. 算法正确性:通过反证法可以证明双指针法能找到最优解

7.2 算法思维提升

  1. 问题转化能力:将几何问题转化为数组操作问题
  2. 双指针应用:掌握在有序或部分有序情况下使用双指针
  3. 贪心策略设计:理解为什么移动较短边是最优选择
  4. 证明能力:学会证明算法正确性的基本方法

7.3 实际应用场景

  1. 水库设计:确定大坝的最佳位置以最大化蓄水量
  2. 城市规划:建筑物之间的空间利用最大化
  3. 货运优化:集装箱或货车的装载优化
  4. 数据分析:在时间序列中寻找最佳区间

面试建议

  1. 首选解法:双指针法,需要详细解释正确性
  2. 解题步骤
    • 明确问题:容器面积由宽度和最小高度决定
    • 提出双指针思路:从最宽容器开始
    • 解释移动策略:为什么移动较短边
    • 证明正确性:使用反证法或直观解释
    • 分析复杂度:时间O(n),空间O(1)
相关推荐
im_AMBER18 小时前
Leetcode 99 删除排序链表中的重复元素 | 合并两个链表
数据结构·笔记·学习·算法·leetcode·链表
源代码•宸19 小时前
Leetcode—1123. 最深叶节点的最近公共祖先【中等】
经验分享·算法·leetcode·职场和发展·golang·dfs
alphaTao20 小时前
LeetCode 每日一题 2026/1/5-2026/1/11
算法·leetcode
黎雁·泠崖20 小时前
二叉树知识体系全梳理:从基础到进阶一站式通关
c语言·数据结构·leetcode
Cx330❀20 小时前
【优选算法必刷100题】第43题(模拟):数青蛙
c++·算法·leetcode·面试
妹妹够啦20 小时前
1. 两数之和
数据结构·算法·leetcode
云里雾里!21 小时前
LeetCode 744. 寻找比目标字母大的最小字母 | 从低效到最优的二分解法优化
算法·leetcode
源代码•宸21 小时前
Leetcode—865. 具有所有最深节点的最小子树【中等】
开发语言·经验分享·后端·算法·leetcode·golang·dfs
Tisfy21 小时前
LeetCode 0865.具有所有最深节点的最小子树:深度优先搜索(一次DFS + Python5行)
算法·leetcode·深度优先·dfs·题解