LeetCode 11|盛最多水的容器:从暴力枚举到双指针优化 | 星雨笔录

本题求数组两线间“短板×宽度”的最大值。暴力法枚举全以 LeetCode 11「盛最多水的容器」为例,从最直观的暴力枚举出发,逐步推导标准双指针与双指针剪枝三种解法。结合短板效应、算法正确性证明、黑盒与白盒测试、详细执行步骤及复杂度分析,深入理解双指针算法的优化原理,并提供 C、C++、Java、Python、JavaScript 五种语言的完整注释实现。部组合(O(n²)),双指针法则通过每次移动较短的指针向中心靠拢,以 O(n) 时间高效求解,是本题最优解。

一、题目描述

题目: LeetCode 11. 盛最多水的容器(中等)

给定一个长度为 n 的非负整数数组 height。数组中第 i 个数表示一块竖直木板的高度,这块木板位于横坐标 i。从中选择两块不同的木板,连同它们之间的 x 轴组成一个容器,求容器最多能够盛多少水。

注意:木板只能竖直放置,容器不能倾斜;没有被选中的中间木板不影响计算。

1.1 输入、输出与计算公式

  • 输入: 整数数组 height,其中 height[i] 是下标 i 对应的木板高度。
  • 输出: 最大盛水量(以单位宽度和单位高度形成的面积表示)。
  • 下标约束: 必须选择 0 <= i < j < n 的两块木板。

容器的宽度是两块木板的横坐标之差:j - i;水面的高度由较矮的木板决定:min(height[i], height[j])。因此:

我们需要求:

为什么取较短的木板?想象左侧木板高 8,右侧木板高 7:水位一旦超过 7,就会从右侧溢出;即使左侧还有 1 个单位高度,也装不住更多的水。

1.2 题目示例

输入:height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
输出:49

解释:选择下标 1 和下标 8:
左侧高度 = 8,右侧高度 = 7
容器宽度 = 8 - 1 = 7
有效水位 = min(8, 7) = 7
盛水面积 = 7 × 7 = 49

1.3 约束条件

  • n == height.length
  • 2 <= n <= 10^5
  • 0 <= height[i] <= 10^4

读题时需要注意的数据规模: 若枚举所有两块木板的组合,需要检查 n(n-1)/2 对。n = 10^5 时,组合数达到约 50 亿,因此我们需要寻找比两层循环更高效的方法。

在本题给定约束下,面积最大不超过 (10^5 - 1) × 10^4 = 999,990,000,可以使用 C/C++/Java 的 32 位 int 保存;如果推广到更大的高度或更长的数组,才需要考虑使用更宽的整数类型。

二、测试用例

写算法前,先弄清楚哪些输入必须得到哪些结果。下面先按题意设计黑盒测试,再针对程序内部条件分支设计白盒测试。三种解法都应该通过同一组测试。

2.1 黑盒测试:根据输入输出验证结果

黑盒测试不关心代码怎么写,只检查程序输出是否符合题意。使用等价类划分与边界值分析,覆盖普通输入、最短数组、零高度、重复高度及较大数据规模。

编号 输入height 期望输出 测试目的
B01 [1,8,6,2,5,4,8,3,7] 49 官方经典样例,最优边界不一定在数组两端
B02 [1,1] 1 最小长度,恰好两块木板
B03 [0,0] 0 所有高度为零
B04 [4,3,2,1,4] 16 两端较高,中间较低
B05 [1,2,1] 2 最优解为两端,宽度较重要
B06 [2,2,2,2] 6 所有高度相等
B07 [0,2,0,3] 4 高度中存在零,但答案非零
B08 [5,0,0,0,5] 20 中间木板不影响选中两端的面积
B09 [1,2,3,4,5] 6 严格递增,最优解不取第一块
B10 [5,4,3,2,1] 6 严格递减
B11 [8,8,8,8] 24 相等高度的指针移动分支
B12 [10000,0,0,10000] 30000 高度上限及乘法

大规模测试: 长度为 100000 的数组,每项均为 10000,期望结果为 999990000。这组数据还可以验证优化算法是否能够在合理时间内完成,而不是采用平方级枚举。

注意:n < 2、负高度或 null 不属于本题规定的合法输入范围,可以在实际工程接口中另行规定行为;不要为了处理题目未要求的情况,使主要算法逻辑变复杂。

2.2 白盒测试:检查条件分支与循环行为

白盒测试关注代码内部是否执行了预期的分支。针对后文的双指针算法,可以重点检查:

编号 输入 关注的执行路径
W01 [1,3] 左边较矮,移动left
W02 [3,1] 右边较矮,移动right
W03 [3,3] 两边相等,执行约定的相等分支
W04 [0,0] 当前面积为零,循环仍能正常结束
W05 [1,8,6,2,5,4,8,3,7] 多轮移动、不断比较并更新最大面积
W06 [1,1,1,1,9] 剪枝法连续跳过无法改进答案的高度

这些是覆盖目标和测试数据,不等同于已经用覆盖率工具证明了 100% 分支覆盖。实际执行时可以为 left、right、area、maxArea 加断点,观察每一轮的变化。

另外可以进行随机对拍:随机生成长度为 2~30、高度为 0~20 的数组,用暴力枚举作为参考结果,和两种双指针算法逐次比较。如果任一次结果不同,就说明至少有一种实现存在错误。这能补充固定用例不容易发现的边界组合。

三、解法一:暴力枚举——把所有可能的答案算一遍

3.1 核心思想

既然答案来自两块木板,就先不急着优化:把所有不同的木板组合都检查一遍,算出每一组能够盛多少水,再选出最大的面积。

例如有 4 块木板 [1, 3, 2, 4],可选的下标组合有 (0,1)、(0,2)、(0,3)、(1,2)、(1,3)、(2,3),总共 6 组。只要这些组合全部考虑,就一定不会漏掉最大值。

这也是解决最值问题时最容易想到的起点:先保证正确,再分析哪些计算可以省略。

3.2 执行步骤

  1. 定义 maxArea = 0,记录目前找到的最大盛水面积;还没有检查任何组合时,初始值设为零。
  2. 从左到右选择第一块木板,下标记为 i。由于右侧至少还要存在一块木板,所以 i 最多遍历到 n - 2。
  3. 对当前 i,依次选择它右边的第二块木板,下标 j 从 i + 1 到 n - 1。这样既不会选到同一块木板,也不会重复枚举 (j, i)。
  4. 计算两块木板间的宽度 j - i,再取两块木板高度的较小值,二者相乘得到当前面积 area。
  5. 如果 area 大于已经记录的 maxArea,就更新 maxArea;否则不变。
  6. 内层循环结束后,换下一个 i,继续寻找新的组合。全部组合处理完毕,返回 maxArea。

以 [1,3,2,4] 为例:

木板下标(i,j) 较矮高度 宽度 当前面积 遍历至此的最大面积
(0,1) 1 1 1 1
(0,2) 1 2 2 2
(0,3) 1 3 3 3
(1,2) 2 1 2 3
(1,3) 3 2 6 6
(2,3) 2 1 2 6

最终结果为 6,由下标 1 和 3 的两块木板形成。

3.3 代码实现

下面五种语言采用相同的两层循环逻辑。每份代码均使用 LeetCode 的函数签名,可以单独提交,不需要额外添加 main。

::: code-group C|C++|Java|Python|JavaScript

int maxArea(int* height, int heightSize) {
    // maxArea 保存已经检查过的组合中最大的盛水面积
    int maxArea = 0;

    // i 表示左侧木板;必须给右侧至少留一块木板
    for (int i = 0; i < heightSize - 1; i++) {
        // j 表示右侧木板,从 i + 1 开始避免重复与自身配对
        for (int j = i + 1; j < heightSize; j++) {
            // 水位由较矮的木板决定
            int minHeight = height[i] < height[j]
                ? height[i] : height[j];

            // 容器面积 = 较矮高度 × 两块木板之间的距离
            int area = minHeight * (j - i);

            // 如果当前组合更好,就记录这个答案
            if (area > maxArea) {
                maxArea = area;
            }
        }
    }

    // 所有左右木板组合已枚举完毕
    return maxArea;
}
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    int maxArea(vector<int>& height) {
        int maxArea = 0;        // 保存目前最大的面积
        int n = height.size();  // 木板数量

        // 外层循环固定左侧木板 i
        for (int i = 0; i < n - 1; ++i) {
            // 内层循环尝试 i 右边的所有木板 j
            for (int j = i + 1; j < n; ++j) {
                // 较矮木板决定水位,距离决定宽度
                int minHeight = min(height[i], height[j]);
                int width = j - i;
                int area = minHeight * width;

                // 每检查一个组合,就更新目前最优答案
                maxArea = max(maxArea, area);
            }
        }

        return maxArea;
    }
};
class Solution {
    public int maxArea(int[] height) {
        int maxArea = 0;          // 当前找到的最大盛水面积
        int n = height.length;    // 木板总数

        // 第一层循环:确定左边界 i
        for (int i = 0; i < n - 1; i++) {
            // 第二层循环:枚举 i 右侧的每一块木板 j
            for (int j = i + 1; j < n; j++) {
                // 水位取决于两块木板中较矮的一块
                int minHeight = Math.min(height[i], height[j]);

                // 相邻下标差就是宽度,而不是木板数量
                int width = j - i;
                int area = minHeight * width;

                // 如果这个容器更大,就覆盖旧答案
                maxArea = Math.max(maxArea, area);
            }
        }

        // 两层循环结束,所有合法下标组合都已检查
        return maxArea;
    }
}
from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        max_area = 0               # 当前最大的盛水面积
        n = len(height)            # 木板的数量

        # 固定左边界 i,最后一块木板不能单独作为左边界
        for i in range(n - 1):
            # 从 i 右侧依次枚举所有可能的右边界 j
            for j in range(i + 1, n):
                # 水位由较矮木板决定,宽度为 j - i
                min_height = min(height[i], height[j])
                width = j - i
                area = min_height * width

                # 保存迄今为止检查过的最大面积
                max_area = max(max_area, area)

        return max_area
/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
    let maxArea = 0;           // 保存已找到的最大面积
    const n = height.length;  // 木板数量

    // 外层循环决定左侧木板的位置
    for (let i = 0; i < n - 1; i++) {
        // 内层循环枚举其右边所有合法木板
        for (let j = i + 1; j < n; j++) {
            // 容器高度由短板决定,容器宽度由下标差决定
            const minHeight = Math.min(height[i], height[j]);
            const width = j - i;
            const area = minHeight * width;

            // 新面积更大时更新结果
            maxArea = Math.max(maxArea, area);
        }
    }

    return maxArea;
};

:::

3.4 复杂度分析

  • 时间复杂度: O(n²)。外层循环执行 n - 1 次,内层依次执行 n - 1、n - 2、...、1 次,总枚举次数为 n(n - 1) / 2,属于平方级。
  • 空间复杂度: O(1)。除了 i、j、area、maxArea 等固定数量变量,没有申请随 n 增长的辅助数组。

暴力法的优点是正确性直观,也适合作为随机对拍的参考实现;问题是当 n 达到 10^5 时,组合数过大,不能满足通常的执行时间要求。

接下来需要回答的问题是:既然许多组合不可能比当前答案更好,能不能直接跳过它们?

四、解法二:双指针——只移动较矮的木板

4.1 核心思想

暴力法最大的浪费,在于它不加区分地尝试每一种宽度和高度组合。实际上,对于某一对左右木板,我们可以利用面积公式排除一批不可能更优的选择。

容器面积为:

先让两块木板处在数组最左端和最右端,此时宽度最大。之后如果要尝试新的组合,指针只能向中间移动,宽度必然缩小。若想获得更大的面积,新的较矮高度就必须足够高,弥补宽度的损失。

关键问题是:左边和右边,应该舍弃哪块木板?

假设左边高度是 3,右边高度是 8,宽度是 6,当前面积就是 3 × 6 = 18。

  • 如果把右边的高木板向左移动,左侧高度 3 不变,新宽度小于 6,所以新面积最多只能是 3 × 5 = 15,不可能超过 18。
  • 如果把左边的矮木板向右移动,新木板有机会超过 3,虽然宽度缩短了,却有可能用更高的水位取得更大面积。

因此得到规则:每次算完面积,移动高度较小的一侧;两边相等时移动任意一侧即可。

4.2 执行步骤

  1. 令 left = 0、right = n - 1,指向最左和最右的两块木板;用 maxArea = 0 记录最大面积。
  2. 只要 left < right,说明两块木板仍然不同,就继续执行循环。
  3. 读取两侧高度 height[left]、height[right];取其中较小的作为水位,同时计算宽度 right - left。
  4. 两者相乘得到当前面积 area,并用它更新 maxArea。先计算当前组合,后移动指针,否则就会漏掉本轮容器。
  5. 比较两侧高度:如果左侧较矮,就执行 left++;否则执行 right--。高度相等时这里约定移动右侧,换成移动左侧也正确。
  6. 重复计算与移动,直到 left >= right。两指针相遇后宽度为零,没有继续计算的意义,返回 maxArea。

和两层枚举最大的不同是:这里不会重新把左右指针往外移动,也不会回头检查已经排除的组合。

4.3 手工模拟

以经典示例 [1,8,6,2,5,4,8,3,7] 为例:

轮次 left right 两侧高度 当前面积 maxArea 下一步
1 0 8 1, 7 1×8 = 8 8 左边短,left++
2 1 8 8, 7 7×7 = 49 49 右边短,right--
3 1 7 8, 3 3×6 = 18 49 right--
4 1 6 8, 8 8×5 = 40 49 等高,约定right--
5 1 5 8, 4 4×4 = 16 49 right--
6 1 4 8, 5 5×3 = 15 49 right--
7 1 3 8, 2 2×2 = 4 49 right--
8 1 2 8, 6 6×1 = 6 49 right--

两指针最终相遇,最大面积仍然是 49。可见当前面积变小是正常现象,不能因此结束搜索;我们真正保存的是所有已检查组合中的最大值。

4.4 为什么移动短板不会漏掉最优解?

这一步是双指针算法的正确性依据,而不是一个只能背诵的口诀。

设当前 height[left] <= height[right],即左侧是短板。当前容器面积:

如果仍然使用左侧这块木板,而把右侧边界换成任何更靠左的位置 k(left < k < right),就有:

由于 min(height[left],height[k]) <= height[left],而且 k-left < right-left,所以:

也就是说:对于当前这块左侧短板,向内寻找任何右边界,都不可能比刚刚检查过的当前组合更好。 既然当前面积已经和 maxArea 比较过,就可以安全舍弃这块左侧短板。

当右侧较矮时,论证完全对称。若两侧高度相等,两种舍弃方向都成立。

每一次移动都至少排除一块已证明无需继续参与搜索的边界,最终留下的最大值不会错过全局最优解。

4.5 流程图

4.6 代码实现

五种语言保持同样的处理顺序:初始化指针 → 计算面积 → 更新最大值 → 移动短板 → 返回答案。

::: code-group C|C++|Java|Python|JavaScript

int maxArea(int* height, int heightSize) {
    // 两个指针分别指向最左、最右的木板
    int left = 0;
    int right = heightSize - 1;
    int maxArea = 0;  // 记录已经找到的最大面积

    // 两个指针不能重合:必须由两块不同木板形成容器
    while (left < right) {
        // 宽度:当前两块木板的下标距离
        int width = right - left;

        // 水位:只能达到左右两块木板中较矮的一块
        int minHeight = height[left] < height[right]
            ? height[left] : height[right];

        // 先计算当前容器的面积,再考虑移动哪一侧
        int area = width * minHeight;
        if (area > maxArea) {
            maxArea = area;
        }

        // 舍弃短板:保留长板,才有机会提高新的有效水位
        if (height[left] < height[right]) {
            left++;
        } else {
            // 右边较矮或两边等高,统一移动右指针
            right--;
        }
    }

    return maxArea; // 全部可能改进答案的边界已检查完毕
}
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0;                        // 最左侧木板
        int right = (int)height.size() - 1;  // 最右侧木板
        int maxArea = 0;                     // 当前最大面积

        while (left < right) {
            // 当前容器的宽度 = 两个下标之差
            int width = right - left;

            // 较矮的一侧限制了容器能够盛水的高度
            int minHeight = min(height[left], height[right]);
            int area = width * minHeight;

            // 不论面积是否增加,都先完成本轮比较
            maxArea = max(maxArea, area);

            // 每次只移动一个指针,因此不会遗漏已排除的组合
            if (height[left] < height[right]) {
                ++left;   // 左边更矮,舍弃左边
            } else {
                --right;  // 右边更矮或等高,舍弃右边
            }
        }

        return maxArea;
    }
};
class Solution {
    public int maxArea(int[] height) {
        // 初始化两端的指针,先考虑宽度最大的组合
        int left = 0;
        int right = height.length - 1;
        int maxArea = 0;

        // 指针相遇时无法再形成有宽度的容器
        while (left < right) {
            // 容器宽度由下标之差决定
            int width = right - left;

            // 水面的最高位置由较矮木板决定
            int minHeight = Math.min(height[left], height[right]);

            // 计算本轮面积,保留更大的那个答案
            int area = minHeight * width;
            maxArea = Math.max(maxArea, area);

            // 宽度必然缩小,所以优先尝试提高短板高度
            if (height[left] < height[right]) {
                left++;   // 左侧较矮,移动左指针
            } else {
                right--;  // 右侧较矮或相等,移动右指针
            }
        }

        // 返回整个搜索过程中出现过的最大面积
        return maxArea;
    }
}
from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        left = 0                  # 初始左边界
        right = len(height) - 1  # 初始右边界
        max_area = 0             # 已知的最大面积

        # 保证左右指针指向两块不同的木板
        while left < right:
            # 当前宽度与短板高度决定了盛水面积
            width = right - left
            min_height = min(height[left], height[right])
            area = width * min_height

            # 无论面积变大还是变小,都与历史最大值比较
            max_area = max(max_area, area)

            # 只有舍弃较矮的一边,才可能找到更高的短板
            if height[left] < height[right]:
                left += 1    # 移动左边界
            else:
                right -= 1   # 移动右边界(包含等高情况)

        return max_area
/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
    let left = 0;                // 左指针:最左侧木板
    let right = height.length - 1; // 右指针:最右侧木板
    let maxArea = 0;             // 搜索过程中的最大盛水面积

    while (left < right) {
        // 先得到当前容器的宽度与短板高度
        const width = right - left;
        const minHeight = Math.min(height[left], height[right]);

        // 面积 = 宽度 × 短板高度
        const area = width * minHeight;
        maxArea = Math.max(maxArea, area);

        // 舍弃短板,逐步缩小搜索区间
        if (height[left] < height[right]) {
            left++;   // 左边更矮,左指针右移
        } else {
            right--;  // 右边更矮或等高,右指针左移
        }
    }

    return maxArea;
};

:::

4.7 复杂度分析

  • 时间复杂度: O(n)。每轮至少有一个指针向中间移动一次,right - left 从 n - 1 逐步减少到 0,恰好进行 n - 1 轮,不再枚举平方级组合。
  • 空间复杂度: O(1)。仅使用 left、right、width、area、maxArea 等有限变量,没有额外数组或哈希表。

相较于暴力法,这次优化真正降低了时间复杂度的数量级:把两层枚举压缩成一次从两端向中间的扫描。这就是本题最值得掌握的标准解法。

五、解法三:双指针剪枝——连续跳过无效高度

5.1 核心思想

标准双指针每一轮只移动一个位置。但我们还可以问:如果移动后遇到的木板不比刚刚舍弃的短板更高,还值得单独计算面积吗?

假设当前左边较矮,且左边高度为 3。计算完面积后,我们决定向右移动左指针。如果接下来连续出现高度 1、2、3,这些木板都不超过已经舍弃的高度 3,但右边界保持不变、宽度继续缩小。

这些位置产生的面积不仅不可能超过刚才算出的面积,而且在后续继续收缩右边界时也不可能构成更好的答案。因此可以直接跳过所有高度小于或等于原短板的木板,直到找到一块更高的木板,或者两个指针已经相遇。

这里的剪枝不是改变最终结果,而是减少部分数据上的无意义面积计算。它与标准双指针一样都是 O(n),没有进一步降低渐近时间复杂度。

5.2 执行步骤

  1. 初始化 left = 0、right = n - 1、maxArea = 0。
  2. 在 left < right 时,计算当前宽度和短板高度,得出 area,并更新 maxArea。
  3. 如果 height[left] < height[right],先保存左侧短板的高度 oldHeight = height[left],然后向右移动左指针。
  4. 移动左指针后,只要 left < right 且新的 height[left] <= oldHeight,就继续移动左指针;这些高度不超过旧短板、宽度却更小的木板不值得再单独计算。
  5. 如果右侧高度小于或等于左侧,执行对称操作:保存右侧旧高度 oldHeight = height[right],向左移动右指针,并连续跳过高度 <= oldHeight 的木板。
  6. 当找到有机会提高短板高度的新位置时,重新计算面积;两指针相遇后返回 maxArea。

例如 [1,1,1,1,9]:

  • 第一次计算最外侧容器:短板高度 1、宽度 4,面积为 4。
  • 左边是短板,保存 oldHeight = 1。
  • 向右移动时,新的左侧木板连续为 1、1、1,都不高于原来的短板,可连续跳过。
  • 指针最终相遇,返回 4。标准双指针会进行 4 次面积计算,而该剪枝版本只进行 1 次。

5.3 为什么可以连续跳过?

设当前待舍弃的短板高度为 H,当前两边界相距 W,我们已经计算过面积 H × W。对于向内移动后遇到的某块木板,若其高度 h <= H,且未来左右边界只会继续收缩,则它参与形成的任何后续容器都满足:

所以这些组合不可能优于刚刚检查过的面积,可以安全跳过。注意剪枝条件必须是相对于移动前保存的旧短板高度判断,而不是随意和当前另一侧进行比较。

5.4 代码实现

下面使用 oldHeight 表示本轮刚刚处理过的短板高度。内层 while 只是让指针跳过无效位置,不需要回退指针,也不需要额外存储数据。

::: code-group C|C++|Java|Python|JavaScript

int maxArea(int* height, int heightSize) {
    int left = 0;
    int right = heightSize - 1;
    int maxArea = 0;

    while (left < right) {
        // 先计算当前两块木板形成的容器面积
        int minHeight = height[left] < height[right]
            ? height[left] : height[right];
        int area = minHeight * (right - left);
        if (area > maxArea) {
            maxArea = area;
        }

        if (height[left] < height[right]) {
            // 左侧是短板:记住它的旧高度,作为剪枝阈值
            int oldHeight = height[left];
            // 高度没有超过 oldHeight 的左侧木板都可以跳过
            do {
                left++;
            } while (left < right && height[left] <= oldHeight);
        } else {
            // 右侧是短板,或两侧等高:对称地移动右指针
            int oldHeight = height[right];
            do {
                right--;
            } while (left < right && height[right] <= oldHeight);
        }
    }

    return maxArea;
}
#include <vector>
#include <algorithm>
using namespace std;

class Solution {
public:
    int maxArea(vector<int>& height) {
        int left = 0;
        int right = (int)height.size() - 1;
        int maxArea = 0;

        while (left < right) {
            // 当前面积仍然必须先计算,不能跳过边界对
            int minHeight = min(height[left], height[right]);
            int area = minHeight * (right - left);
            maxArea = max(maxArea, area);

            if (height[left] < height[right]) {
                // 旧左短板决定本次可跳过的高度范围
                int oldHeight = height[left];
                do {
                    ++left;
                } while (left < right && height[left] <= oldHeight);
            } else {
                // 旧右短板决定本次可跳过的高度范围
                int oldHeight = height[right];
                do {
                    --right;
                } while (left < right && height[right] <= oldHeight);
            }
        }

        return maxArea;
    }
};
class Solution {
    public int maxArea(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int maxArea = 0;

        while (left < right) {
            // 第一步:先计算当前左右木板能形成的面积
            int minHeight = Math.min(height[left], height[right]);
            int area = minHeight * (right - left);
            maxArea = Math.max(maxArea, area);

            // 第二步:根据短板所在的一侧进行剪枝
            if (height[left] < height[right]) {
                // 保存旧左短板,后续遇到不高于它的木板直接跳过
                int oldHeight = height[left];
                do {
                    left++;
                } while (left < right && height[left] <= oldHeight);
            } else {
                // 保存旧右短板,连续跳过无法提高水位的位置
                int oldHeight = height[right];
                do {
                    right--;
                } while (left < right && height[right] <= oldHeight);
            }
        }

        // 剪枝仅省略不可能改进答案的组合,不改变正确结果
        return maxArea;
    }
}
from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        left = 0
        right = len(height) - 1
        max_area = 0

        while left < right:
            # 先计算当前两侧木板形成的实际面积
            min_height = min(height[left], height[right])
            area = min_height * (right - left)
            max_area = max(max_area, area)

            if height[left] < height[right]:
                # 左侧为短板,后续只关心能否找到更高的左木板
                old_height = height[left]
                left += 1
                # 跳过所有不高于旧短板的位置
                while left < right and height[left] <= old_height:
                    left += 1
            else:
                # 右侧较矮或两侧等高:对称处理右边界
                old_height = height[right]
                right -= 1
                while left < right and height[right] <= old_height:
                    right -= 1

        return max_area
/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function(height) {
    let left = 0;
    let right = height.length - 1;
    let maxArea = 0;

    while (left < right) {
        // 每轮首先计算当前左右边界所能得到的面积
        const minHeight = Math.min(height[left], height[right]);
        const area = minHeight * (right - left);
        maxArea = Math.max(maxArea, area);

        if (height[left] < height[right]) {
            // 左边较矮:连续跳过不高于旧短板的木板
            const oldHeight = height[left];
            do {
                left++;
            } while (left < right && height[left] <= oldHeight);
        } else {
            // 右边较矮或相等:连续跳过无效的右侧木板
            const oldHeight = height[right];
            do {
                right--;
            } while (left < right && height[right] <= oldHeight);
        }
    }

    return maxArea;
};

:::

5.5 复杂度分析

  • 时间复杂度: O(n)。虽然出现了嵌套的 while,但 left 只会向右走、right 只会向左走,整个程序中两个指针移动的总次数最多为 n - 1,因此不是 O(n²)。
  • 空间复杂度: O(1)。只增加了一个保存旧短板高度的变量 oldHeight,不依赖输入规模。

性能上的真实取舍: 剪枝版本在某些高度分布中会减少面积计算次数,例如大量相等或连续较低的木板;但它也加入了额外的条件判断,不保证在所有语言、运行环境和数据分布下都比标准双指针更快。实际面试与生产代码中通常优先采用更简单的标准双指针,只有在测试或性能分析表明确有收益时才采用这一写法。

六、总结:不同情况下选择哪一种解法?

比较维度 暴力枚举 标准双指针 双指针剪枝
核心思路 检查每一对木板 从两端向内移动短板 移动短板并跳过不高于旧短板的位置
时间复杂度 O(n²) O(n) O(n)
额外空间复杂度 O(1) O(1) O(1)
是否适合n = 10^5 否 是 是
主要价值 容易理解、便于校验其他算法 简洁、高效,面试首选 进一步认识剪枝与常数开销
推荐场景 小数据、算法推导、随机对拍 LeetCode 提交、面试、常规开发 学习优化思想、针对性性能实验

整个优化过程可以概括为:

暴力枚举先保证不遗漏任何组合;接着发现面积被短板限制,于是让双指针从最大宽度开始,每轮有依据地舍弃短板;最后再利用“新高度没有变高且宽度还在变窄”的条件,得到跳过无效高度的剪枝版本。

但要明确:前两种方法之间是从 O(n²) 到 O(n) 的复杂度优化;后两种方法之间只是同一复杂度等级下的计算步骤优化。代码行数更多、检查次数更少,并不自动意味着实际运行时间更短。

本题最值得记住的三个判断

  1. 什么时候想到暴力枚举? 题目要求从若干候选组合中求最大值,而暂时还不知道哪些组合可以排除时,先枚举所有合法组合,建立正确的参考解。
  2. 什么时候想到双指针? 候选答案由左右边界决定,且有能够证明“移动一侧不会错过最优解”的单调性或排除规律时,可以尝试从两端向内搜索。
  3. 什么时候考虑剪枝? 已经能够证明某些候选不可能改进已知结果时,再跳过这些候选;证明正确性比减少几行计算代码更重要。

本题一句话记忆:宽度向内移动必然变小,要争取更大的面积,就必须给短板变高的机会,因此每轮移动较矮的一侧。

进一步练习可以选择 LeetCode 42. 接雨水 与 LeetCode 15. 三数之和。它们也能训练双指针思维,但移动规则和证明方法不同,不能直接照搬本题的“移动短板”。