bigywater.tech
算法与数据结构 · LeetCode 11

LeetCode 11. 盛最多水的容器

给定一个数组 height,第 i 个元素表示第 i 根竖线的高度。选两根竖线和 x 轴围成一个容器,容量是 min(两根线的高度) × 两根线的下标之差,求最大容量。暴力做法是枚举所有线对,时间 O(n²)。双指针做法从最宽的一对(两端)开始,每一步只移动较矮的那根线:宽度必然变小,只有让较矮的那根换成更高的线,容量才可能变大,这样一趟扫描 O(n) 就能找到答案。本文实测暴力解与双指针在不同规模下的耗时,验证"移动较高的线"这个直觉写法会出什么错,给出五种语言的实现,并把双指针的移动过程做成分步演示。

20,000
组随机对拍,与暴力解的结果全部一致
10 步
分步演示,每一帧都来自解法的真实执行,另附 5 种语言实现

题意与示例

完整题目见官方页面(链接在文末)。前几组基准用例和结果由暴力解(枚举所有线对)算出。一共 11 组,覆盖:只有两根线、两根线高度为 0、最优解在两端、最优解在中间、高度递增或递减、全部等高:

真实实测 · 本机执行输出
输入 [[1,8,6,2,5,4,8,3,7]]
输出 49
输入 [[1,1]]
输出 1
输入 [[4,3,2,1,4]]
输出 16
输入 [[1,2,1]]
输出 2
输入 [[0,0]]
输出 0
输入 [[1,2,4,3]]
输出 4

为什么只能移动较矮的那根线

容量 = 较矮的线的高度 × 宽度。假设当前两端是 l 和 r,且 height[l] < height[r]:如果移动较高的 r,新的一对线里较矮的仍然不会比 height[l] 更高,而宽度还变小了,容量不可能变大;所以以 l 为左端的所有更窄的线对都可以直接放弃,把 l 向右移动是安全的。反过来,如果移动较高的那根线,就可能错过最优解。下面是"移动较高的线"的写法(height[l] > height[r] 时移动左边)与正确写法在几个输入上的真实结果:

真实实测 · 本机执行输出
# 移动长板 vs 移动短板
height=[1, 8, 6, 2, 5, 4, 8, 3, 7]: 移动长板 -> 8   solve -> 49   暴力解 -> 49
height=[1, 1]: 移动长板 -> 1   solve -> 1   暴力解 -> 1
height=[4, 3, 2, 1, 4]: 移动长板 -> 16   solve -> 16   暴力解 -> 16
height=[1, 2, 1]: 移动长板 -> 2   solve -> 2   暴力解 -> 2
height=[1, 2, 4, 3]: 移动长板 -> 3   solve -> 4   暴力解 -> 4

在官方示例 [1, 8, 6, 2, 5, 4, 8, 3, 7] 上,移动较高线的写法只得到 8,而正确答案是 49(下标 1 和下标 8 两根线);[1, 2, 4, 3] 上得到 3,正确答案是 4。但在另外几个较小的用例上两种写法恰好给出相同的结果,所以只用几个简单示例很难发现这个逻辑错误,需要和暴力解做对拍才能可靠地暴露。

双指针:五种语言的实现

左右指针分别指向两端,每一步先计算当前容量并更新最大值,再移动较矮的那一端。Python、Java、Go、JavaScript、C++ 五种语言都是真实编译运行的;Go 用的是 Go 1.21 起内置的 min 和 max 函数。

Python · 解法代码def solve(height): l, r = 0, len(height) - 1 best = 0 while l < r: best = max(best, min(height[l], height[r]) * (r - l)) if height[l] < height[r]: # 短板决定容量:移动长板只会让宽度变小、高度不会变大,所以只能移动短板 l += 1 else: r -= 1 return best
真实实测 · Python · 前几组用例的输出
$ python3 solution.py   # 统一入口:逐行读取 cases.jsonl,调用上面的 solve,打印 JSON
[[1,8,6,2,5,4,8,3,7]]  →  49
[[1,1]]  →  1
[[4,3,2,1,4]]  →  16
[[1,2,1]]  →  2
[[0,0]]  →  0
[[1,2,4,3]]  →  4
Java · 解法代码class Solution { public int maxArea(int[] height) { int l = 0, r = height.length - 1, best = 0; while (l < r) { best = Math.max(best, Math.min(height[l], height[r]) * (r - l)); if (height[l] < height[r]) l++; // 短板决定容量:只能移动短板 else r--; } return best; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java Solution.java && java -cp build Main < cases.jsonl
[[1,8,6,2,5,4,8,3,7]]  →  49
[[1,1]]  →  1
[[4,3,2,1,4]]  →  16
[[1,2,1]]  →  2
[[0,0]]  →  0
[[1,2,4,3]]  →  4
Go · 解法代码func maxArea(height []int) int { l, r, best := 0, len(height)-1, 0 for l < r { best = max(best, min(height[l], height[r])*(r-l)) if height[l] < height[r] { // 短板决定容量:只能移动短板 l++ } else { r-- } } return best }
真实实测 · Go · 前几组用例的输出
$ go build -o bin . && ./bin < cases.jsonl
[[1,8,6,2,5,4,8,3,7]]  →  49
[[1,1]]  →  1
[[4,3,2,1,4]]  →  16
[[1,2,1]]  →  2
[[0,0]]  →  0
[[1,2,4,3]]  →  4
JavaScript · 解法代码function maxArea(height) { let l = 0, r = height.length - 1, best = 0; while (l < r) { best = Math.max(best, Math.min(height[l], height[r]) * (r - l)); if (height[l] < height[r]) l++; // 短板决定容量:只能移动短板 else r--; } return best; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js < cases.jsonl
[[1,8,6,2,5,4,8,3,7]]  →  49
[[1,1]]  →  1
[[4,3,2,1,4]]  →  16
[[1,2,1]]  →  2
[[0,0]]  →  0
[[1,2,4,3]]  →  4
C++ · 解法代码#include <algorithm> #include <vector> using namespace std; class Solution { public: int maxArea(vector<int>& height) { int l = 0, r = (int)height.size() - 1, best = 0; while (l < r) { best = max(best, min(height[l], height[r]) * (r - l)); if (height[l] < height[r]) l++; // 短板决定容量:只能移动短板 else r--; } return best; } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin < cases.jsonl
[[1,8,6,2,5,4,8,3,7]]  →  49
[[1,1]]  →  1
[[4,3,2,1,4]]  →  16
[[1,2,1]]  →  2
[[0,0]]  →  0
[[1,2,4,3]]  →  4
真实实测 · 本机执行输出
共 11 组用例(含边界),以暴力解的结果为基准逐组比对:
  Python      通过 11/11
  Java        通过 11/11
  Go          通过 11/11
  JavaScript  通过 11/11
  C++         通过 11/11

复杂度实测:平方对线性

用一个伪随机的高度数组,每个规模取三次中最快的一次:

真实实测 · 本机执行输出
数组长度 n | 暴力解 | 本文解法
      1000 |    38.419 ms | 0.088 ms
      5000 |  1035.943 ms | 0.440 ms
     20000 | 16208.529 ms | 6.073 ms
   1000000 |      (未测,太慢) | 99.949 ms

数组长度从 1000 增加到 5000(5 倍),暴力解耗时从 38.9ms 涨到 1270.5ms,增长约 33 倍;从 5000 增加到 20000(4 倍),涨到 16383.7ms,增长约 13 倍,符合 O(n²)。双指针从 1000 到 5000 增长约 4.4 倍,从 5000 到 20000 增长约 4.2 倍,到 1000000 是 103.6ms,是线性增长。1000000 长度的暴力解没有测,按上面的趋势要跑很久。

随机对拍

Python 版本另外和暴力解做了随机对拍:

真实实测 · 本机执行输出
随机对拍 20000 组(长度 2 到 14、高度 0 到 30 的随机数组(重复高度很多)),solve 与 brute 的结果全部一致。

另外四种语言只在上面 11 组固定用例上验证过,没有做大规模随机对拍。

分步演示:双指针怎么向中间靠拢

下面的演示用官方示例 height = [1, 8, 6, 2, 5, 4, 8, 3, 7]。每一帧都是把上面的解法在这组输入上真实执行记录下来的,运行器还检查了演示最终得到的答案与 solve 的返回值一致。蓝色是当前的两根线,被排除的线会变淡,琥珀色是目前最大容量对应的两根线。

盛最多水的容器:height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
当前两块板目前最大容量对应的两块板已经被排除的板

    参考与说明

    • 官方题目页:LeetCode 11. 盛最多水的容器。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚;耗时对比只用 Python 测量。
    • 没有覆盖:双指针正确性的严格证明(上面只给了直觉性的论证,真实证据是随机对拍);存在多个相同容量最优解时选择哪一对(题目只要求容量)。
    京ICP备2026067502号-1京公网安备11010502063487号