给定一个数组 height,第 i 个元素表示第 i 根竖线的高度。选两根竖线和 x 轴围成一个容器,容量是 min(两根线的高度) × 两根线的下标之差,求最大容量。暴力做法是枚举所有线对,时间 O(n²)。双指针做法从最宽的一对(两端)开始,每一步只移动较矮的那根线:宽度必然变小,只有让较矮的那根换成更高的线,容量才可能变大,这样一趟扫描 O(n) 就能找到答案。本文实测暴力解与双指针在不同规模下的耗时,验证"移动较高的线"这个直觉写法会出什么错,给出五种语言的实现,并把双指针的移动过程做成分步演示。
完整题目见官方页面(链接在文末)。前几组基准用例和结果由暴力解(枚举所有线对)算出。一共 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
$ 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; } }
$ 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 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; }
$ 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; } };
$ 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 的返回值一致。蓝色是当前的两根线,被排除的线会变淡,琥珀色是目前最大容量对应的两根线。