【LeetCode 刷题系列|第 2 篇】详解盛最多水的容器:从暴力到双指针的优化之路💧

63 阅读12分钟

🌊 前言

各位掘金的小伙伴们,咱们刷题系列又见面啦!今天要攻克的是 LeetCode 上的经典中等题 ——「盛最多水的容器」。这道题和上一篇讲的「接雨水」虽然都和 “水” 有关,但思路却大不相同:接雨水是算所有低洼能存多少水,而这道题是找两个柱子之间能装最多水的区域。看似简单的问题,却藏着从暴力到高效解法的巧妙优化,尤其适合理解 “双指针” 的核心思想。话不多说,咱们一步步拆解,让你彻底搞懂这道题~

一、LeetCode 盛最多水的容器题目详情

1. 题目描述

给定一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

说明:你不能倾斜容器,且 n 的值至少为 2。

题目链接11. 盛最多水的容器 - 力扣(LeetCode)

2. 示例演示

  • 输入:height = [1,8,6,2,5,4,8,3,7]
  • 输出:49
  • 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49(由索引 1 的 8 和索引 8 的 7 组成,宽度 7,高度取较矮的 7,7×7=49)。
  • 输入:height = [1,1]
  • 输出:1

3. 难度级别

🟠 中等:这道题的难点在于如何从 O (n²) 的暴力解法优化到 O (n) 的高效解法,核心是理解 “为什么双指针移动能保留最优解”。

二、解题思路大剖析

1. 暴力解法:枚举所有可能的容器

暴力解法的思路很直接:枚举所有可能的两根柱子组合,计算它们能容纳的水量,取最大值

核心公式:

容器的水量 = 两根柱子之间的宽度 × 两根柱子中较矮的高度

即:area = (j - i) × Math.min(height[i], height[j])(其中 i < j)

分步拆解演示(以输入 [1,8,6,2,5,4,8,3,7] 为例):

我们需要遍历所有 i < j 的组合:

  • i=0, j=1:宽度 1,高度 min (1,8)=1 → 面积 1×1=1;
  • i=0, j=2:宽度 2,高度 min (1,6)=1 → 面积 2×1=2;
  • i=0, j=3:宽度 3,高度 min (1,2)=1 → 面积 3×1=3;
  • ...
  • i=1, j=8:宽度 7(8-1=7),高度 min (8,7)=7 → 面积 7×7=49;
  • i=6, j=8:宽度 2(8-6=2),高度 min (8,7)=7 → 面积 2×7=14;
  • 所有组合计算完成后,最大值为 49。

JavaScript 代码实现(暴力解法):

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
    const n = height.length;
    let maxArea = 0; // 存储最大面积,初始为0
    // 外层循环:枚举左柱子(索引i从0到n-2)
    for (let i = 0; i < n - 1; i++) {
        // 内层循环:枚举右柱子(索引j从i+1到n-1)
        for (let j = i + 1; j < n; j++) {
            // 计算当前组合的宽度(j - i)和有效高度(较矮的柱子高度)
            const width = j - i;
            const validHeight = Math.min(height[i], height[j]);
            // 计算当前容器面积
            const currentArea = width * validHeight;
            // 更新最大面积(取当前面积和历史最大面积的较大值)
            maxArea = Math.max(maxArea, currentArea);
        }
    }
    return maxArea;
};
// 测试用例验证
console.log(maxArea([1,8,6,2,5,4,8,3,7])); // 输出49,符合预期
console.log(maxArea([1,1])); // 输出1,符合预期

暴力解法的优缺点:

  • 优点:思路简单直观,无需复杂逻辑,新手容易理解和实现;
  • 缺点:时间复杂度为 O (n²)(两层循环,枚举所有组合),当 n 超过 10^4 时会触发超时,无法通过 LeetCode 的所有测试用例。

2. 双指针解法:高效缩小范围找最优解

双指针的核心思路是通过左右指针从数组两端向中间移动,每次舍弃较短的柱子,保留可能产生更大面积的组合,从而将时间复杂度优化到 O (n)(仅需遍历一次数组)。

核心原理:

  1. 初始状态:左指针 left 指向数组最左侧(索引 0),右指针 right 指向数组最右侧(索引 n-1),此时两根柱子的宽度最大(n-1),是所有组合中宽度的最大值;
  1. 面积计算:根据核心公式计算当前指针组合的面积,更新最大面积;
  1. 指针移动规则谁矮就移动谁——
    • 若 height[left] < height[right]:左柱子更矮,移动左指针(left++);
    • 若 height[left] >= height[right]:右柱子更矮(或等高),移动右指针(right--);
  1. 终止条件:当 left >= right 时,指针相遇,所有可能的有效组合已遍历完成,循环终止。

关键疑问:为什么 “谁矮移谁” 能保留最优解?

假设当前左指针 i 对应的柱子高度为 h[i],右指针 j 对应的柱子高度为 h[j],且 h[i] < h[j]:

  • 此时容器的有效高度由 h[i] 决定(因为高度取较矮值),宽度为 j - i,面积为 (j - i) * h[i];
  • 若移动右指针 j 向左(j--),新的宽度变为 j' - i(j' = j-1),宽度一定减小;而有效高度最多还是 h[i](因为左柱子 i 没动,且 h[i] 是较矮的),所以新面积 (j' - i) * min(h[i], h[j']) <= (j' - i) * h[i] < (j - i) * h[i],即新面积一定小于当前面积;
  • 若移动左指针 i 向右(i++),虽然宽度减小,但可能遇到更高的左柱子(h[i'] > h[i]),此时有效高度可能增大,从而获得比当前更大的面积;
  • 综上,移动较矮的指针是唯一可能找到更大面积的选择,移动较高的指针则一定不会得到更优解,因此 “谁矮移谁” 的规则是合理的。

三、双指针解法分步拆解(带图解 + 每步细节)

以测试用例 height = [1,8,6,2,5,4,8,3,7] 为例,逐轮拆解指针移动、面积计算和最大面积更新的全过程:

1. 初始状态说明

  • 数组长度 n=9;
  • 指针位置:left=0(指向高度 1),right=8(指向高度 7);
  • 最大面积:maxArea=0(初始值)。

2. 初始状态图解

索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
      L                    R  (L=0,R=8)
当前宽度:8-0=8
当前有效高度:min(1,7)=1
当前面积:8×1=8
maxArea:max(0,8)=8

3. 逐轮遍历详细步骤

第 1 轮:left=0,right=8

  • 高度对比:height[0]=1 < height[8]=7(左指针更矮);
  • 指针移动:left++ → left=1(指向高度 8);
  • 计算当前面积:宽度 8-1=7,有效高度 min(8,7)=7,面积 7×7=49;
  • 更新最大面积:maxArea = max(8,49)=49;
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L                  R  (L=1,R=8)
maxArea:49

第 2 轮:left=1,right=8

  • 高度对比:height[1]=8 > height[8]=7(右指针更矮);
  • 指针移动:right-- → right=7(指向高度 3);
  • 计算当前面积:宽度 7-1=6,有效高度 min(8,3)=3,面积 6×3=18;
  • 更新最大面积:maxArea = max(49,18)=49(无变化);
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L              R  (L=1,R=7)
maxArea:49

第 3 轮:left=1,right=7

  • 高度对比:height[1]=8 > height[7]=3(右指针更矮);
  • 指针移动:right-- → right=6(指向高度 8);
  • 计算当前面积:宽度 6-1=5,有效高度 min(8,8)=8,面积 5×8=40;
  • 更新最大面积:maxArea = max(49,40)=49(无变化);
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L            R  (L=1,R=6)
maxArea:49

第 4 轮:left=1,right=6

  • 高度对比:height[1]=8 == height[6]=8(两根柱子等高);
  • 指针移动:任意移动(此处选择移动右指针)→ right-- → right=5(指向高度 4);
  • 计算当前面积:宽度 5-1=4,有效高度 min(8,4)=4,面积 4×4=16;
  • 更新最大面积:maxArea = max(49,16)=49(无变化);
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L        R  (L=1,R=5)
maxArea:49

第 5 轮:left=1,right=5

  • 高度对比:height[1]=8 > height[5]=4(右指针更矮);
  • 指针移动:right-- → right=4(指向高度 5);
  • 计算当前面积:宽度 4-1=3,有效高度 min(8,5)=5,面积 3×5=15;
  • 更新最大面积:maxArea = max(49,15)=49(无变化);
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L      R  (L=1,R=4)
maxArea:49

第 6 轮:left=1,right=4

  • 高度对比:height[1]=8 > height[4]=5(右指针更矮);
  • 指针移动:right-- → right=3(指向高度 2);
  • 计算当前面积:宽度 3-1=2,有效高度 min(8,2)=2,面积 2×2=4;
  • 更新最大面积:maxArea = max(49,4)=49(无变化);
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L    R  (L=1,R=3)
maxArea:49

第 7 轮:left=1,right=3

  • 高度对比:height[1]=8 > height[3]=2(右指针更矮);
  • 指针移动:right-- → right=2(指向高度 6);
  • 计算当前面积:宽度 2-1=1,有效高度 min(8,6)=6,面积 1×6=6;
  • 更新最大面积:maxArea = max(49,6)=49(无变化);
  • 本轮图解:
索引:0 1 2 3 4 5 6 7 8
高度:1 8 6 2 5 4 8 3 7
        L  R  (L=1,R=2)
maxArea:49
第 8 轮:left=1,right=2
  • 高度对比:height[1]=8 > height[2]=6(右指针更矮);
  • 指针移动:right-- → right=1;
  • 终止判断:此时 left=1,right=1,满足 left >= right,循环终止;
  • 最终最大面积:49。

4. 最终结果

最大面积为 49,与测试用例 height = [1,8,6,2,5,4,8,3,7] 的预期输出一致,验证双指针解法的正确性。

四、JavaScript 代码实现(双指针解法)

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
    let left = 0; // 左指针:初始指向数组最左侧(索引0)
    let right = height.length - 1; // 右指针:初始指向数组最右侧(索引n-1)
    let maxArea = 0; // 存储最大面积,初始为0
    // 循环:当左指针小于右指针时,继续遍历
    while (left < right) {
        // 1. 计算当前指针组合的宽度和有效高度
        const width = right - left; // 宽度 = 右指针索引 - 左指针索引
        const validHeight = Math.min(height[left], height[right]); // 有效高度 = 两根柱子的较矮值
        
        // 2. 计算当前面积,并更新最大面积
        const currentArea = width * validHeight;
        maxArea = Math.max(maxArea, currentArea);
        // 3. 指针移动:谁矮移谁
        if (height[left] < height[right]) {
            left++; // 左柱子更矮,左指针右移
        } else {
            right--; // 右柱子更矮(或等高),右指针左移
        }
    }
    return maxArea; // 返回最大面积
};
// 测试用例验证
console.log(maxArea([1,8,6,2,5,4,8,3,7])); // 输出49,符合预期
console.log(maxArea([1,1])); // 输出1,符合预期
console.log(maxArea([4,3,2,1,4])); // 输出16(索引0和4,宽度4,高度4,4×4=16)

五、复杂度分析

1. 时间复杂度

  • 暴力解法:O (n²),两层嵌套循环,枚举所有 i < j 的组合,组合数量为 n(n-1)/2,时间复杂度与 n² 成正比;
  • 双指针解法:O (n),左右指针从两端向中间移动,每个指针最多移动 n-1 次,总遍历次数为 O(n),时间复杂度与数组长度成正比。

2. 空间复杂度

  • 暴力解法:O (1),仅使用 maxArea、width、validHeight 等常数级变量,额外空间不随数组长度变化;
  • 双指针解法:O (1),仅使用 left、right、maxArea 等常数级变量,额外空间同样为常数级。

六、总结与拓展

1. 方法对比与推荐

解法时间复杂度空间复杂度适用场景推荐度
暴力解法O(n²)O(1)理解题意、新手入门⭐⭐
双指针解法O(n)O(1)面试答题、实际项目、LeetCode 提交⭐⭐⭐⭐⭐

面试推荐:优先选择双指针解法,它不仅效率高,而且逻辑清晰,能体现对 “优化枚举” 思路的理解,是面试官期望看到的解法。

2. 核心逻辑提炼

双指针解法的本质是 “舍次保优”:通过初始时选择最大宽度的组合,后续每次舍弃 “不可能产生更优解” 的较矮柱子,在宽度减小的同时,尽可能寻找更高的柱子来提升有效高度,从而在一次遍历中找到最优解。这种思路在很多 “枚举优化” 类问题中都有应用。

3. 拓展思考

类似的 “双指针优化暴力枚举” 问题:

  • LeetCode 15. 三数之和(排序后用双指针将 O (n³) 优化为 O (n²));
  • LeetCode 167. 两数之和 II - 输入有序数组(双指针从两端向中间移动,O (n) 时间复杂度);
  • LeetCode 42. 接雨水(双指针优化空间复杂度,从 O (n) 降至 O (1))。

建议大家尝试用 “双指针” 思路解决这些问题,加深对 “指针移动规则” 的理解,做到举一反三。

七、互动环节

今天的「盛最多水的容器」就讲解到这里啦!相信大家已经掌握了双指针的核心逻辑 ——“谁矮移谁”。如果在分步拆解过程中还有疑问,或者有其他优化思路(比如是否有更巧妙的指针移动方式),欢迎在评论区留言讨论~

下一篇专栏,咱们会继续攻克 LeetCode 高频题(可能是 “三数之和” 或 “无重复字符的最长子串”),关注我,刷题路上不迷路!咱们下期再见~ 👋