Java详解LeetCode 热题 100(05):LeetCode 11. 盛最多水的容器(Container With Most Water)详解

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. 理解题目

这道题可以理解为:

  1. 有 n 根不同高度的柱子,分别位于 x 轴上的位置 0, 1, 2, …, n-1
  2. 选择两根柱子,它们构成一个容器
  3. 容器的宽度是两根柱子之间的距离(坐标差)
  4. 容器的高度是两根柱子中较短的那根的高度(木桶效应)
  5. 容器的容量是宽度 × 高度
  6. 我们要找出哪两根柱子组成的容器,可以盛最多的水

关键点:

  • 水的容量受限于较短的柱子
  • 容器不能倾斜,因此两根柱子之间的其他柱子对容量没有影响
  • 宽度由两根柱子的索引差决定

3. 解法一:暴力法

3.1 思路

最直观的方法是枚举所有可能的柱子对,计算每对柱子能形成的容器容量,然后找出最大值:

  1. 使用两层循环,遍历所有可能的柱子对
  2. 对于每一对柱子 (i, j),计算它们构成的容器的容量:
    • 宽度 = j - i
    • 高度 = min(height[i], height[j])
    • 容量 = 宽度 × 高度
  3. 记录并返回最大容量

3.2 Java代码实现

public class Solution {
    public int maxArea(int[] height) {
        int maxWater = 0;
        int n = height.length;
        
        // 枚举所有可能的柱子对
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                // 计算容器的宽度
                int width = j - i;
                // 计算容器的高度(取两柱子的较小值)
                int h = Math.min(height[i], height[j]);
                // 计算容器的容量
                int water = width * h;
                // 更新最大容量
                maxWater = Math.max(maxWater, water);
            }
        }
        
        return maxWater;
    }
}

3.3 代码详解

  1. 初始化 maxWater 为0,用于记录最大容量
  2. 使用两层循环遍历所有可能的柱子对:
    • 外层循环 i 从0到 n-1
    • 内层循环 j 从 i+1 到 n-1
  3. 对于每一对柱子 (i, j):
    • 计算容器的宽度:width = j - i
    • 计算容器的高度:h = Math.min(height[i], height[j])
    • 计算容器的容量:water = width * h
  4. 如果当前容量大于已知的最大容量,则更新最大容量
  5. 最后返回最大容量

3.4 复杂度分析

  • 时间复杂度:O(n²),其中 n 是数组的长度。需要两层循环来枚举所有的柱子对。
  • 空间复杂度:O(1),只需要常数级别的额外空间。

3.5 适用场景

暴力法简单直观,适合小规模数据和教学目的。但对于较大规模的数组(例如题目中提到的 n 可达 10^5),这种方法可能会超时。

4. 解法二:双指针法

4.1 思路

双指针法是解决此问题的最优解,其核心思想是:

  1. 使用两个指针,初始时分别指向数组的两端
  2. 计算这两个指针指向的柱子构成的容器容量
  3. 然后移动指向较短柱子的那个指针(向内移动)
  4. 重复步骤2和3,直到两个指针相遇
  5. 在整个过程中记录最大容量

为什么要移动较短的那个指针?因为:

  • 容器的容量受限于较短的柱子
  • 如果移动较长的柱子,容器的宽度会减小,而高度不会增加(仍然受限于较短的柱子),所以容量只会减小
  • 而移动较短的柱子,虽然宽度减小,但高度可能增加,容量可能增大

4.2 Java代码实现

public class Solution {
    public int maxArea(int[] height) {
        int maxWater = 0;
        int left = 0;                  // 左指针,初始指向数组第一个元素
        int right = height.length - 1; // 右指针,初始指向数组最后一个元素
        
        while (left < right) {
            // 计算当前两个指针构成的容器容量
            int width = right - left;
            int h = Math.min(height[left], height[right]);
            int water = width * h;
            
            // 更新最大容量
            maxWater = Math.max(maxWater, water);
            
            // 移动指向较短柱子的指针
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        
        return maxWater;
    }
}

4.3 代码详解

  1. 初始化 maxWater 为0,用于记录最大容量
  2. 使用两个指针 leftright,分别指向数组的首尾
  3. left < right 时,执行循环:
    • 计算当前两个指针构成的容器宽度:width = right - left
    • 计算容器的高度:h = Math.min(height[left], height[right])
    • 计算容器的容量:water = width * h
    • 更新最大容量:maxWater = Math.max(maxWater, water)
    • 移动指向较短柱子的指针:
      • 如果 height[left] < height[right],移动左指针:left++
      • 否则,移动右指针:right--
  4. 返回最大容量

4.4 复杂度分析

  • 时间复杂度:O(n),其中 n 是数组的长度。我们只需遍历一次数组。
  • 空间复杂度:O(1),只需要常数级别的额外空间。

4.5 正确性证明

为什么双指针法是正确的?我们需要证明在移动指针的过程中不会错过最优解。

假设当前左右指针分别为 leftright,高度分别为 height[left]height[right],我们假设 height[left] < height[right](较短的柱子在左边)。

如果最优解包含 left 指针所指的柱子,那么我们已经计算过了当前的容量。

如果最优解不包含 left 指针所指的柱子,而是某个位置 i(其中 left < i < right)与 right 形成的容器,那么:

  • 这个容器的宽度为 right - i,小于当前容器的宽度 right - left
  • 这个容器的高度为 min(height[i], height[right])
  • 由于 height[left] < height[right],要使新容器的容量更大,必须 height[i] > height[left]
  • 但即使 height[i] 远大于 height[left],容器的高度最多为 height[right]
  • 所以新容器的容量为 (right - i) * min(height[i], height[right])
  • 这个容量不可能超过 (right - left) * height[right],而后者大于我们当前计算的容量 (right - left) * height[left]

因此,在 height[left] < height[right] 的情况下,移动 left 指针不会错过最优解。同理,在 height[left] >= height[right] 的情况下,移动 right 指针也不会错过最优解。

4.6 适用场景

双指针法是解决此问题的最优解,适用于所有情况,尤其是大型数组。这种方法既高效又简洁。

5. 解法三:双指针法的变体

5.1 思路

我们可以对双指针法做一些优化:

  1. 当移动指针时,如果发现新指向的柱子高度小于或等于原来的高度,可以继续移动指针,因为这样形成的容器容量只会更小。
  2. 这样可以跳过一些不必要的计算,进一步提高效率。

5.2 Java代码实现

public class Solution {
    public int maxArea(int[] height) {
        int maxWater = 0;
        int left = 0;
        int right = height.length - 1;
        
        while (left < right) {
            int width = right - left;
            int h = Math.min(height[left], height[right]);
            int water = width * h;
            
            maxWater = Math.max(maxWater, water);
            
            // 移动较短的一侧,并跳过不会产生更大容量的柱子
            if (height[left] < height[right]) {
                int currentLeft = height[left];
                while (left < right && height[left] <= currentLeft) {
                    left++;
                }
            } else {
                int currentRight = height[right];
                while (left < right && height[right] <= currentRight) {
                    right--;
                }
            }
        }
        
        return maxWater;
    }
}

5.3 代码详解

  1. 初始化 maxWater 为0,用于记录最大容量
  2. 使用两个指针 leftright,分别指向数组的首尾
  3. left < right 时,执行循环:
    • 计算当前两个指针构成的容器容量
    • 更新最大容量
    • 根据以下策略移动指针:
      • 如果 height[left] < height[right]
        • 记录当前左柱子的高度 currentLeft
        • 移动左指针直到找到高度大于 currentLeft 的柱子或指针相遇
      • 否则:
        • 记录当前右柱子的高度 currentRight
        • 移动右指针直到找到高度大于 currentRight 的柱子或指针相遇
  4. 返回最大容量

5.4 复杂度分析

  • 时间复杂度:O(n),虽然有循环嵌套,但每个位置最多只被访问一次,所以总体时间复杂度仍为O(n)。
  • 空间复杂度:O(1),只需要常数级别的额外空间。

5.5 优点与缺点

优点

  • 通过跳过不会产生更大容量的柱子,减少了不必要的计算
  • 在某些特殊情况下(如数组中有很多相同高度的柱子),性能会比基本的双指针法更好

缺点

  • 代码稍微复杂一些
  • 在随机数据下,性能提升可能不明显

6. 特殊情况和边界处理

在实现解决方案时,我们需要考虑以下特殊情况:

  1. 只有两根柱子:此时只有一种可能,直接计算容量即可。

    if (height.length == 2) {
        return Math.min(height[0], height[1]);
    }
    
  2. 所有柱子高度相同:在这种情况下,最大容量将由最远的两根柱子决定。

    • 双指针法会正确地计算这种情况,无需特殊处理
  3. 单调递增或递减的柱子:例如[1,2,3,4,5]或[5,4,3,2,1]

    • 双指针法会从两端开始,逐渐向中间移动,确保不会错过最优解
  4. 高度为0的柱子:这种柱子不会贡献任何容量,但双指针法会自动跳过它们

7. 优化与改进

7.1 提前结束条件

在某些情况下,我们可以提前终止搜索:

public int maxArea(int[] height) {
    int maxWater = 0;
    int left = 0;
    int right = height.length - 1;
    
    while (left < right) {
        int width = right - left;
        int h = Math.min(height[left], height[right]);
        int water = width * h;
        
        maxWater = Math.max(maxWater, water);
        
        // 提前结束条件:如果剩余宽度 * 最大可能高度都小于当前最大容量,可以提前结束
        if ((right - left) * Math.max(height[left], height[right]) <= maxWater) {
            break;
        }
        
        if (height[left] < height[right]) {
            left++;
        } else {
            right--;
        }
    }
    
    return maxWater;
}

注意:这个优化在实际中可能并不总是有效,因为额外的检查可能会抵消节省的计算。

7.2 记忆化搜索

对于暴力法,我们可以使用记忆化搜索避免重复计算:

public int maxArea(int[] height) {
    // 创建记忆数组,memo[i][j]表示柱子i和j之间的最大容量
    int[][] memo = new int[height.length][height.length];
    return findMaxArea(height, 0, height.length - 1, memo);
}

private int findMaxArea(int[] height, int left, int right, int[][] memo) {
    // 基础情况
    if (left >= right) return 0;
    
    // 如果已经计算过,直接返回结果
    if (memo[left][right] > 0) return memo[left][right];
    
    // 计算当前柱子对形成的容器容量
    int currentArea = (right - left) * Math.min(height[left], height[right]);
    
    // 选择移动左指针或右指针,取决于哪个柱子更短
    int nextArea;
    if (height[left] < height[right]) {
        nextArea = findMaxArea(height, left + 1, right, memo);
    } else {
        nextArea = findMaxArea(height, left, right - 1, memo);
    }
    
    // 记录并返回最大容量
    memo[left][right] = Math.max(currentArea, nextArea);
    return memo[left][right];
}

注意:这种方法的时间复杂度为O(n²),空间复杂度也为O(n²),在大规模数据下仍不如双指针法高效。

8. 完整的 Java 解决方案

以下是最优解决方案(双指针法)的完整实现:

class Solution {
    public int maxArea(int[] height) {
        int maxWater = 0;
        int left = 0;
        int right = height.length - 1;
        
        while (left < right) {
            // 计算当前容器容量
            int width = right - left;
            int h = Math.min(height[left], height[right]);
            int water = width * h;
            
            // 更新最大容量
            maxWater = Math.max(maxWater, water);
            
            // 移动指向较短柱子的指针
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        
        return maxWater;
    }
}

9. 实际应用与扩展

9.1 应用场景

盛最多水的容器问题的思想在实际中有多种应用:

  1. 资源优化:在一些资源分配问题中,可能需要选择两个点来最大化它们之间的某种度量
  2. 图像处理:在某些图像处理算法中,可能需要找出图像中能含有最多像素的矩形区域
  3. 物流规划:在物流仓库设计中,可能需要确定最优的货架位置以最大化存储空间
  4. 网络带宽分配:在网络设计中,可能需要确定两个节点之间的最大可能带宽

9.2 扩展问题

  1. 三维容器问题:如果扩展到三维空间,如何找到容积最大的容器?

    public int maxVolume(int[][] heights) {
        // 这里heights[i][j]表示在坐标(i,j)处的高度
        // 实现三维最大容积的计算
        // 这是一个复杂的问题,可能需要更高级的算法
    }
    
  2. 有漏洞的容器:如果容器中可能有漏洞(某些位置不能存水),如何计算最大容量?

    public int maxAreaWithHoles(int[] height, boolean[] hasHole) {
        int maxWater = 0;
        int left = 0;
        int right = height.length - 1;
        
        while (left < right) {
            // 跳过有漏洞的位置
            if (hasHole[left]) {
                left++;
                continue;
            }
            if (hasHole[right]) {
                right--;
                continue;
            }
            
            // 计算当前容器容量
            int width = right - left;
            int h = Math.min(height[left], height[right]);
            int water = width * h;
            
            maxWater = Math.max(maxWater, water);
            
            if (height[left] < height[right]) {
                left++;
            } else {
                right--;
            }
        }
        
        return maxWater;
    }
    
  3. 动态变化的容器:如果柱子的高度可能随时间变化,如何高效地重新计算最大容量?

    public class DynamicContainer {
        private int[] heights;
        private int maxWater;
        
        public DynamicContainer(int[] initialHeights) {
            this.heights = initialHeights.clone();
            this.maxWater = calculateMaxWater();
        }
        
        public void updateHeight(int index, int newHeight) {
            heights[index] = newHeight;
            // 重新计算最大容量
            maxWater = calculateMaxWater();
        }
        
        private int calculateMaxWater() {
            // 使用双指针算法计算最大容量
            int maxWater = 0;
            int left = 0;
            int right = heights.length - 1;
            
            while (left < right) {
                int width = right - left;
                int h = Math.min(heights[left], heights[right]);
                int water = width * h;
                
                maxWater = Math.max(maxWater, water);
                
                if (heights[left] < heights[right]) {
                    left++;
                } else {
                    right--;
                }
            }
            
            return maxWater;
        }
        
        public int getMaxWater() {
            return maxWater;
        }
    }
    

10. 常见问题与解答

10.1 为什么暴力法会超时?

暴力法需要O(n²)的时间复杂度,当n很大时(如n=105),计算量会达到1010,这在大多数编程语言中都会超出时间限制。

10.2 双指针法为什么能保证找到最优解?

双指针法的核心思想是,每次移动指向较短柱子的指针。这是因为容器的容量受限于较短的柱子,而移动较长的柱子只会减小宽度而不会增加高度,从而容量只会减小。通过这种方式,我们可以保证不会错过最优解。

10.3 如何处理大规模数据?

对于非常大的数组:

  • 使用双指针法,时间复杂度为O(n)
  • 如果数据分布有特点,可以考虑使用跳过相似高度的优化版本
  • 对于特别大的数据集,可以考虑并行处理不同区间,然后合并结果

10.4 如何验证算法的正确性?

可以使用以下测试用例:

  • 基本测试:[1,8,6,2,5,4,8,3,7] → 49
  • 单调递增:[1,2,3,4,5] → 4 (柱子1和5,宽度为4,高度为1)
  • 单调递减:[5,4,3,2,1] → 4 (柱子1和5,宽度为4,高度为1)
  • 全部相同:[5,5,5,5,5] → 20 (柱子1和5,宽度为4,高度为5)
  • 两个高峰:[1,8,1,1,1,8,1] → 30 (柱子2和6,宽度为4,高度为8)

11. 测试用例

为了验证解决方案的正确性,以下是一些测试用例:

public class ContainerWithMostWaterTest {
    public static void main(String[] args) {
        Solution solution = new Solution();
        
        // 测试用例1:标准测试
        int[] height1 = {1, 8, 6, 2, 5, 4, 8, 3, 7};
        testAndPrint(solution, height1, "测试用例1");
        
        // 测试用例2:只有两个柱子
        int[] height2 = {1, 2};
        testAndPrint(solution, height2, "测试用例2");
        
        // 测试用例3:单调递增
        int[] height3 = {1, 2, 3, 4, 5};
        testAndPrint(solution, height3, "测试用例3");
        
        // 测试用例4:单调递减
        int[] height4 = {5, 4, 3, 2, 1};
        testAndPrint(solution, height4, "测试用例4");
        
        // 测试用例5:全部相同高度
        int[] height5 = {5, 5, 5, 5, 5};
        testAndPrint(solution, height5, "测试用例5");
        
        // 测试用例6:两个高峰
        int[] height6 = {1, 8, 1, 1, 1, 8, 1};
        testAndPrint(solution, height6, "测试用例6");
        
        // 测试用例7:零高度
        int[] height7 = {0, 5, 0, 5, 0};
        testAndPrint(solution, height7, "测试用例7");
    }
    
    private static void testAndPrint(Solution solution, int[] height, String caseName) {
        int result = solution.maxArea(height);
        System.out.println(caseName + ":");
        System.out.println("输入: " + Arrays.toString(height));
        System.out.println("输出: " + result);
        System.out.println();
    }
}

12. 图解算法

为了更好地理解这个问题,下面是一个图解示例(使用文本表示):

示例数组:[1,8,6,2,5,4,8,3,7]

   |                                      
   |                                      
 8 |    |              |                  
   |    |              |                  
 7 |    |              |        |         
   |    |              |        |         
 6 |    |  |           |        |         
   |    |  |           |        |         
 5 |    |  |     |     |        |         
   |    |  |     |     |        |         
 4 |    |  |     |  |  |        |         
   |    |  |     |  |  |        |         
 3 |    |  |     |  |  |  |     |         
   |    |  |     |  |  |  |     |         
 2 |    |  |  |  |  |  |  |     |         
   |    |  |  |  |  |  |  |     |         
 1 |  | |  |  |  |  |  |  |  |  |         
   ----------------------------------------
      0  1  2  3  4  5  6  7  8  
  1. 初始状态:left = 0, right = 8

    • 容器宽度 = 8 - 0 = 8
    • 容器高度 = min(1, 7) = 1
    • 容器容量 = 8 * 1 = 8
  2. 由于 height[left] < height[right],移动左指针:left = 1, right = 8

    • 容器宽度 = 8 - 1 = 7
    • 容器高度 = min(8, 7) = 7
    • 容器容量 = 7 * 7 = 49
  3. 由于 height[left] > height[right],移动右指针:left = 1, right = 7

    • 容器宽度 = 7 - 1 = 6
    • 容器高度 = min(8, 3) = 3
    • 容器容量 = 6 * 3 = 18

…依此类推,最后找到的最大容量为49。

13. 总结与技巧

13.1 解题要点

  1. 理解容器容量的计算方式:容量 = 宽度 × 高度,其中高度取决于较短的柱子
  2. 双指针技巧的应用:从两端开始,逐步向中间移动,每次移动较短的那一端
  3. 合理剪枝:跳过那些不可能产生更大容量的情况
  4. 处理边界情况:如只有两根柱子、全部相同高度等
  5. 优化思路:从暴力法O(n²)到双指针法O(n)的优化过程

13.2 常用技巧

  1. 双指针法:在处理数组中需要考虑两个元素的问题时,双指针法常常很有效
  2. 贪心策略:每次移动较短的指针,以期望找到更大的容量
  3. 状态转移:通过移动指针,从一个状态转移到另一个状态,并在过程中记录最优解
  4. 分析极限情况:理解在各种特殊情况下算法的行为
  5. 图形化思考:将问题转换为几何问题,可以帮助更好地理解

13.3 面试技巧

在面试中遇到此类问题时:

  1. 先讨论暴力解法,说明你理解问题
  2. 分析暴力解法的时间复杂度,指出其不足
  3. 引入双指针法,解释为什么它能够正确工作
  4. 证明双指针法不会错过最优解
  5. 讨论各种边界情况和优化可能
  6. 如果有时间,可以讨论问题的扩展和应用场景

14. 参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈凯哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值