覆盖 11 盛最多水的容器、15 三数之和、42 接雨水,主线是排序 / 双指针 / 前缀最值 与「短板效应」。
每题统一为:① 题目分析与解题思路 (含出错点)→ ② 常见解法及讲解 → ③ 知识点总结。
11. 盛最多水的容器

① 题目分析与解题思路
- 核心公式 :容量 = 两线间距(宽度)× 两线中较短的高度(短板决定高度),即 area=(j−i)×min(heighti,heightj)area = (j-i) \times \min(heighti, heightj)area=(j−i)×min(heighti,heightj)。
- 目标 :选两个下标 (i,j)(i,j)(i,j) 使容量最大;限制 :n≤105n \le 10^5n≤105。
- 暴力法 :枚举所有两两组合算面积,O(n2)O(n^2)O(n2)。
- 双指针法(最优) :左指针最左、右指针最右(初始宽度最大),每轮移动更矮的那侧 ------移动高的一侧宽度必减、高度上限不变,面积只会更小。直至两指针相遇,O(n)O(n)O(n)。
常见出错点
- 误以为移动高的指针能得更大面积(双指针核心理解错误)。
- 面积写成 max(heighti,heightj)\max(heighti, heightj)max(heighti,heightj),把长板当高度(错!是短板)。
- 宽度算成 jjj,忘记减去 iii。
- 暴力解法不看数据范围,直接超时(就是你现在遇到的)。
② 常见解法及讲解(Java)
解法 1:暴力枚举(仅适合小数据,不推荐)
java
class Solution {
public int maxArea(int[] height) {
int max = 0;
int n = height.length;
for(int i = 0; i < n; i++){
for(int j = i + 1; j < n; j++){
int area = (j - i) * Math.min(height[i], height[j]);
max = Math.max(max, area);
}
}
return max;
}
}
讲解:两层循环枚举所有柱子组合,算面积、记录最大值。时间 O(n2)O(n^2)O(n2)、空间 O(1)O(1)O(1),n 大时超时,只能用来理解题意。
- ✅ 计算逻辑正确,小用例能出正确答案。
- ❌ 致命问题:双重循环 O(n2)O(n^2)O(n2) ,n=105n=10^5n=105 时接近 50 亿次,直接超时;另外
maxHeight、maxlength、maxij这类中间变量不必单独定义,一行算出即可。
解法 2:双指针(Hot 100 标准最优解,推荐背诵)
java
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 h = Math.min(height[left], height[right]);
int area = width * h;
maxArea = Math.max(maxArea, area);
// 移动短板
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
}
讲解:
left=0、right=len-1,此时宽度最大。- 每轮算围成的面积并更新最大值;哪边更矮,就把矮的那侧指针往里移。
- 相遇即结束。时间 O(n)O(n)O(n),空间 O(1)O(1)O(1)。
示例推演 :[1,8,6,2,5,4,8,3,7],初始 8×1=8;左边短 left++ → 7×7=49;右边短 right--;如此反复,一遍扫完得 49。
③ 知识点总结
- 容量 = 宽度 × 较短高度,「短板效应」是全部推导的起点。
- 每轮必须移动短板 ,一次移动等于排除一整批不可能更优的解 ,这是 O(n2)→O(n)O(n^2)\to O(n)O(n2)→O(n) 的关键;循环条件
left < right,width = right - left别忘减left。 - 面试先讲暴力,再讲双指针与正确性论证。
15. 三数之和

① 题目分析与解题思路
题目要求 :找出所有三个不同下标元素、和为 0 的组合,返回的三元组不能重复 ;三元组内顺序、答案顺序均不限,难点是去重。
核心思路 :三重循环 O(n3)O(n^3)O(n3) 会超时,优化为排序 + 固定第一个数 + 双指针找另两个数。
- 先排序:既方便双指针移动,也方便跳重;再固定
nums[i],目标变成在右侧找两数之和等于-nums[i]。 - 令
left = i+1、right = n-1:和偏大right--,偏小left++,相等就记录三元组、两侧同时跳过重复值再收缩。 - 去重两处:
nums[i] == nums[i-1]跳过;命中后 left 右移、right 左移,直到与刚才的值不同。
复杂度 :排序 O(nlogn)O(n\log n)O(nlogn) + 外层循环与双指针 O(n2)O(n^2)O(n2),整体 O(n2)O(n^2)O(n2),可通过 n=3000。
常见出错点
- ❌ 忘记排序,直接双指针:双指针的前提是有序数组。
- ❌ 去重写错:只对 i 去重、命中后 left/right 不跳重,会出重复三元组;i 去重写成
nums[i] == nums[i+1],逻辑颠倒。 - ❌ 指针范围错:left 从 0 开始而非 i+1,i 与 left 下标会相同。
- ❌ 命中后只移动一个指针,陷入死循环。
- ❌ 边界:
[0,0,0]全 0 样例被错误跳过,得不到正确答案。 - ❌ 暴力三重循环:O(n3)O(n^3)O(n3),n=3000 直接超时。
② 常见解法及讲解(Java)
解法 1:排序 + 双指针(推荐,Hot 100 标准解法)
java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 列表里元素是列表
List<List<Integer>> res = new ArrayList<>();
// Arrays.sort 默认按升序排序
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n; i++) {
// 第一个数已经大于0,后面全部更大,不可能凑出和为0,直接break
if (nums[i] > 0) {
break;
}
// i去重:和上一个一样,跳过
// 若用 nums[i] 和 nums[i - 1] 则可能会越界
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = n - 1;
int target = -nums[i];
int sum = 0;
// 双指针每轮只做 O(1) 操作,整个 while 是 O(n)
while (left < right) {
sum = nums[left] + nums[right];
if (sum < target) { // sum和小于目标数,和要变大向右移
left++;
} else if (sum > target) { // sum和大于目标数,数要变小向左移
right--;
} else { // 找到一组答案,添加进列表
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
// left去重,避免重复
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// right去重,避免重复
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
// nums[i], nums[left], nums[right]三者有两个确定,另一个也确定了
// 收缩
left++;
right--;
}
}
}
return res;
}
}
讲解:
Arrays.sort(nums)是整个算法的基础。nums[i] > 0时数组升序、后面全 ≥ 它,三正数不可能和为 0,直接 break 剪枝。i > 0 && nums[i] == nums[i-1]跳过重复首数(i > 0同时防越界);命中后先跳过相邻重复的 left、right 再收缩,两个去重 while 都要带left < right,否则越界。
解法 2:暴力三重循环(不推荐,仅理解)
java
// 超时写法,仅用来理解题意
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
Set<List<Integer>> set = new HashSet<>();
int n = nums.length;
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
for(int k=j+1;k<n;k++){
if(nums[i]+nums[j]+nums[k]==0){
List<Integer> temp = Arrays.asList(nums[i],nums[j],nums[k]);
Collections.sort(temp);
set.add(temp);
}
}
}
}
return new ArrayList<>(set);
}
}
讲解:三层循环枚举所有三元组,用 Set 自动去重;O(n3)O(n^3)O(n3),n=3000 时极慢,无法通过。
解法 3:哈希表(了解即可,去重麻烦)
思路:固定 i,剩下两数之和用哈希表做两数之和;去重非常麻烦,面试优先写双指针版本。
附:Arrays.asList 与「为什么双指针不用 Set」
核心 :Arrays.asList(...) 把几个值包装成固定大小的 List------不能 add/remove(底层是数组包装),但可入 Set、可直接返回。
第一组:暴力写法里这三行
java
List<Integer> temp = Arrays.asList(nums[i],nums[j],nums[k]);
Collections.sort(temp);
set.add(temp);
- 生成时按 i、j、k 的遍历顺序(如
[-1,2,-1]),顺序不固定,同一三元组会以[-1,2,-1]、[-1,-1,2]两种形态出现,在 Set 眼里是两个 List。 Collections.sort(temp)排完变[-1,-1,2],✅ 统一内部顺序 ;set.add(temp)靠元素不重复去重。即「先生成、再过滤」。
第二组:双指针写法里
java
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
- nums 已全局排序且
i < left < right,天然满足nums[i] ≤ nums[left] ≤ nums[right](如[-4,-1,-1,0,1,2]取 i=1、left=2、right=5 得[-1,-1,2])。 - 故既不用
Collections.sort也不用 Set:三处跳重让重复解根本不会生成,直接 add 进 res,属于「先过滤、再生成」。
对比总结
| 方式 | 前置条件 | 排序作用 | 去重方式 | 需要 Set |
|---|---|---|---|---|
| 暴力三重循环 | 数组无序,i/j/k 随机遍历 | 统一三元组内部顺序 | 生成后靠 Set 剔除 | ✅ |
| 排序 + 双指针 | nums 已全局排序 + 指针跳重 | 无需再排 | 循环里提前跳过重复 | ❌ |
高频坑 :Arrays.asList() 返回 java.util.Arrays.ArrayList,可 get、可入 Set、可直接返回,但 ❌ 不能 temp.add() / temp.remove()。要可变 List 用:
plain
new ArrayList<>(Arrays.asList(a,b,c))
延伸(面试常问):能否用 Set 写双指针版?可以但不推荐------多一份空间开销,手动去重就是标准答案。
③ 知识点总结
- 三数之和 = 排序 + 固定一个数 + 双指针求两数之和,O(n2)O(n^2)O(n2)。
- 排序干三件事:双指针可移动、
nums[i] > 0可剪枝、去重只需比相邻值。 - 去重三处:i 与 i-1(
i > 0防越界)、命中后 left / right 跳重,且先跳重再收缩;源头去重就不需要 Set,暴力法只能事后补救。 Arrays.asList返回定长 List,可读不可增删;可变要用new ArrayList<>(...)。
42. 接雨水

① 题目分析与解题思路
核心结论:下标 i 处能接的雨水 = min(左侧最高, 右侧最高) − 当前高度;结果 ≤ 0 表示接不到,取 0;总雨水为各位置累加。
wateri=max(0, min(leftMaxi,rightMaxi)−heighti)wateri = \max(0,\ \min(leftMaxi, rightMaxi) - heighti)wateri=max(0, min(leftMaxi,rightMaxi)−heighti)
leftMax[i]:i 左侧所有柱子的最大值;rightMax[i]:i 右侧所有柱子的最大值。
思路路线 :① 暴力------每个 i 向左右扫描;② 预处理数组------正向存 leftMax、反向存 rightMax 再累加(面试优先能写出来);③ 双指针------维护左右最大值,一趟完成,空间 O(1)O(1)O(1)。
常见出错点
- ❌ 理解错公式:写成 max(leftMax,rightMax)−heighti\max(leftMax, rightMax) - heightimax(leftMax,rightMax)−heighti,应取左右最大值里更小的那个(短板)。
- ❌ 忘记和 0 比较:
min(left,right) - height[i]可能是负数,要取 0。 - ❌ 边界:最左、最右的柱子接不到雨水。
- ❌ 预处理数组初始化错:
leftMax[0] = height[0]、rightMax[n-1] = height[n-1]。 - ❌ 双指针逻辑搞反:何时移动左 / 右判断错误。
- ❌ 累加时忽略每根柱子宽度是 1。
② 常见解法及讲解(Java)
解法 1:暴力法(理解原理,会超时,不推荐)
java
class Solution {
public int trap(int[] height) {
int n = height.length;
int res = 0;
for (int i = 1; i < n - 1; i++) {
int leftMax = 0;
int rightMax = 0;
// 找左侧最高
for (int l = 0; l < i; l++) {
leftMax = Math.max(leftMax, height[l]);
}
// 找右侧最高
for (int r = i + 1; r < n; r++) {
rightMax = Math.max(rightMax, height[r]);
}
// 累加
int cur = Math.min(leftMax, rightMax) - height[i];
if (cur > 0) {
res += cur;
}
}
return res;
}
}
讲解:每个位置单独左右扫描。时间 O(n2)O(n^2)O(n2)、空间 O(1)O(1)O(1);n 到 2e4 时超时,仅用于理解公式。
解法 2:预处理左右最大值数组(DP,推荐先掌握)
java
class Solution {
public int trap(int[] height) {
int n = height.length;
// 数量 < 3 结果为0
if (n <= 2) {
return 0;
}
int res = 0;
// 预处理左右最大值数组
int[] leftMax = new int[n];
int[] rightMax = new int[n];
leftMax[0] = height[0];
// 从前往后,记录各自左侧最大值
for (int i = 1; i < n; i++) {
// 取上一个左侧最大值 和 当前值 中的较大值
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
rightMax[n - 1] = height[n - 1];
// 从后往前,记录各自右侧最大值
for (int j = n - 2; j >= 0; j--) {
// 取 上一个右侧最大值 和 当前值 中的较大值
rightMax[j] = Math.max(rightMax[j + 1], height[j]);
}
for (int k = 0; k < n; k++) {
// 对于边界:左边界:leftMax==height,右边界:rightMax==height,water都为<=0
int water = Math.min(leftMax[k], rightMax[k]) - height[k];
if (water > 0) {
res += water;
}
}
return res;
}
}
讲解:leftMax 从左往右递推 max(上一个左最大值, 当前高度),rightMax 反向同理,最后套公式累加。时间 O(n)O(n)O(n)、空间 O(n)O(n)O(n)。
解法 3:双指针(最优,Hot 100 标准答案,空间 O(1)O(1)O(1))
java
class Solution {
public int trap(int[] height) {
// 左指针,初始在数组最左侧
int left = 0;
// 右指针,初始在数组最右侧
int right = height.length - 1;
// 记录left指针走过区域的最大高度(左边最高柱子)
int leftMax = 0;
// 记录right指针走过区域的最大高度(右边最高柱子)
int rightMax = 0;
// 累加雨水总量
int res = 0;
// 左右指针相遇时停止循环
while (left < right) {
// 当前左柱子高度 < 右柱子高度:左边是短板
// 含义:对于left位置,能装多少水只由leftMax决定,右侧一定存在一个更高的柱子托住水
if (height[left] < height[right]) {
// 如果当前柱子高度 >= 左侧历史最高,更新左侧最高,这个位置不能存水
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
// 当前柱子比左侧最高矮,可以存水:水量 = 左侧最高 - 当前柱子高度
res += leftMax - height[left];
}
// 左指针向右移动
left++;
} else {
// 当前右柱子高度 <= 左柱子高度:右边是短板
// 含义:对于right位置,能装多少水只由rightMax决定,左侧一定存在更高的柱子托住水
if (height[right] >= rightMax) {
// 更新右侧最高,该位置不能存水
rightMax = height[right];
} else {
// 可以存水,水量 = 右侧最高 - 当前柱子高度
res += rightMax - height[right];
}
// 右指针向左移动
right--;
}
}
// 返回总共接住的雨水
return res;
}
}
讲解:
height[left] < height[right]时左边是短板,left 处积水只由 leftMax 决定 (右侧必有更高的柱子托住水);反之处理 right、右指针左移,同步更新leftMax/rightMax。- 核心原理一句话:谁矮谁就是短板,短板一侧的积水上限由自己这侧的历史最大值决定。
- 时间 O(n)O(n)O(n),空间 O(1)O(1)O(1)。
补充:单调栈解法(了解)
思路:栈存下标,栈内高度单调递减;遇到更高柱子弹栈顶算凹槽雨水。复杂度同为 O(n)O(n)O(n),稍难写,一般前两种优先掌握。
③ 知识点总结
- 单点公式
min(左最高, 右最高) - 当前高度(负数取 0)------短板效应,与 11 题同源。 - 三种解法是同一公式的不同落地:暴力(现算)→ 预处理(空间换时间)→ 双指针(变量替代数组);单调栈则是横向视角,同为 O(n)O(n)O(n)。
- 双指针正确性:
height[left] < height[right]时,left 右侧最高 ≥height[right],故只受leftMax约束。 - 边界:首尾接不到水,
n <= 2返回 0;柱子宽按 1 计。