三数之和 图解题解
排序之后一个固定、两端夹,命中后两侧同时收并跳过重复——三数之和的核心就这两步。
排序后固定第一个数,剩下两个从两端往中间夹:三数超了就把右指针左移换小的,不够就把左指针右移换大的,等于零就记录下来——但命中之后 l 和 r 要同时向内走,并且跳过紧跟着的相同值,防止记录重复三元组。固定值本身也要跳重复,否则整组就重了。这两处去重是本题最容易漏的细节。
这道题到底在问什么
- 输入
- nums=[-1,0,1,2,-1,-4]
- 输出
- [[-1,-1,2],[-1,0,1]]
最优解:为什么这么做
一句话答案:LeetCode 15 三数之和的标准解是排序加对撞双指针:先排序,外层固定第一个数,内层用左右指针在剩余区间找「两数之和等于它的相反数」,和小了左指针右移、和大了右指针左移,配合三处跳过相邻重复值完成去重。时间 O(n²)、空间 O(1),比三层暴力的 O(n³) 低一个量级。
这道题真正在问什么
在数组里找出所有满足 a + b + c = 0 的三元组,且结果不能包含重复的三元组——[-1,0,1] 和 [0,1,-1] 算同一个。两个要求叠加:既要找全,又要去重。去重恰恰是这题的真正难点,它直接决定了「先排序」不是可选项而是必选项。
为什么三层暴力行不通
三层循环枚举所有组合是 O(n³),数组一大就跑不动;更麻烦的是去重——乱序数组里同一组数会以不同顺序被枚举到多次,要么把每个三元组排序后塞进集合判重(又慢又占空间),要么根本去不干净。这提示我们先给数组排序:排序后相同的数挤在一起,「跳过相邻重复值」就能完成全部去重;同时有序性还能反过来加速查找,一举两得。
排序后双指针为什么能省掉一层循环
固定第一个数 nums[i] 之后,剩下的问题是「在它右侧的有序区间里找两个数,和等于 0 - nums[i]」——这正是有序数组上的两数之和。左指针放区间头、右指针放区间尾:当前两数之和偏小时,只有左指针右移才可能让和变大;偏大时,只有右指针左移才可能让和变小。每一步都确定性地排除一个不可能参与解的端点,两个指针合计走一遍就穷尽了所有可能,把内层 O(n²) 的组合枚举压成 O(n)。这一步完全依赖有序性——乱序时「和小了往右走」不成立,这是必须先排序的第二个原因。
三处去重分别防住什么
去重有三处,缺一处就会输出重复三元组。外层一处:固定数与前一个固定数相同就跳过,防止两轮固定同一个值、产出完全相同的一批结果。内层两处:命中一个三元组后,左指针跳过与刚才相同的值、右指针同样跳过,防止同一个固定数下反复收到相同的搭档对。还有一个易忽略的动作——命中后左右指针要同时向内移动,只动一个的话另一头的值没变,下一轮要么重复命中要么白走一步。
复杂度怎么算,哪些边界会翻车
排序 O(n log n),外层固定 n 次、内层双指针各是线性,总时间 O(n²);排序原地进行、过程只用几个指针变量,不计输出结果的话空间 O(1)。边界上注意两处:数组不足 3 个数时直接返回空;全是同一个数的输入(比如 [0,0,0])要靠三处去重才能正确地只输出一个 [0,0,0],少一处就会重复。
▶ 动画逐步走查(共 28 步)——想跟着动画一帧帧对照就展开
- 3记住这条「排序 + 固定一个 + 双指针找两数」,下面每帧都在套它。
- 4排序后的数组:[-4,-2,-1,-1,0,1,2,3]。从左到右依次固定第一个数,剩下区间用双指针对撞。
- 5固定第一个数 nums[0]=-4,于是要在它右边找「两数之和 = 4」(也就是 0 减去它)。
- 6双指针就位:左指针 l 指向 1(值 -2),右指针 r 指向末尾 7(值 3),当前两数和 1,目标 4。
- 7两数和比目标 4 小,需要更大的数 → 左指针右扩到 2(值 -1),现在两数和 2。
- 8两数和比目标 4 小,需要更大的数 → 左指针右扩到 3(值 -1),现在两数和 2。
- 9两数和比目标 4 小,需要更大的数 → 左指针右扩到 4(值 0),现在两数和 3。
- 10两数和比目标 4 小,需要更大的数 → 左指针右扩到 5(值 1),现在两数和 4。
- 11命中!-4 + 1 + 3 = 0,收下三元组 [-4,1,3](绿色高亮)。然后两指针同时内移继续找。
- 12固定第一个数 nums[1]=-2,于是要在它右边找「两数之和 = 2」(也就是 0 减去它)。
- 13双指针就位:左指针 l 指向 2(值 -1),右指针 r 指向末尾 7(值 3),当前两数和 2,目标 2。
- 14命中!-2 + -1 + 3 = 0,收下三元组 [-2,-1,3](绿色高亮)。然后两指针同时内移继续找。
- 15命中后跳过和刚才相同的相邻值(l 跳到 4、r 跳到 6),避免收到重复三元组(内层去重)。
- 16命中!-2 + 0 + 2 = 0,收下三元组 [-2,0,2](绿色高亮)。然后两指针同时内移继续找。
- 17固定第一个数 nums[2]=-1,于是要在它右边找「两数之和 = 1」(也就是 0 减去它)。
- 18双指针就位:左指针 l 指向 3(值 -1),右指针 r 指向末尾 7(值 3),当前两数和 2,目标 1。
- 19两数和比目标 1 大,需要更小的数 → 右指针左缩到 6(值 2),现在两数和 1。
- 20命中!-1 + -1 + 2 = 0,收下三元组 [-1,-1,2](绿色高亮)。然后两指针同时内移继续找。
- 21命中!-1 + 0 + 1 = 0,收下三元组 [-1,0,1](绿色高亮)。然后两指针同时内移继续找。
- 22nums[3]=-1 和前一个固定数相同,再固定一遍会得到重复三元组,直接跳过(外层去重)。
- 23固定第一个数 nums[4]=0,于是要在它右边找「两数之和 = 0」(也就是 0 减去它)。
- 24双指针就位:左指针 l 指向 5(值 1),右指针 r 指向末尾 7(值 3),当前两数和 4,目标 0。
- 25两数和比目标 0 大,需要更小的数 → 右指针左缩到 6(值 2),现在两数和 3。
- 26两数和比目标 0 大,需要更小的数 → 右指针左缩到 5(值 1),现在两数和 2。
- 27固定第一个数 nums[5]=1,于是要在它右边找「两数之和 = -1」(也就是 0 减去它)。
- 28双指针就位:左指针 l 指向 6(值 2),右指针 r 指向末尾 7(值 3),当前两数和 5,目标 -1。
- 29两数和比目标 -1 大,需要更小的数 → 右指针左缩到 6(值 2),现在两数和 4。
- 30整趟扫完,所有和为 0 的不重复三元组:[-4,1,3],[-2,-1,3],[-2,0,2],[-1,-1,2],[-1,0,1]。排序 O(n log n) + 外层固定 × 内层双指针 O(n²)。
⚠️ 容易写错的地方
✗ 错:不排序直接双指针
✓ 对:必须先排序
对撞双指针靠「单调」判断往哪移,乱序就失效
✗ 错:忘了三处去重
✓ 对:i、l、r 命中后都要跳相邻重复
不去重会收到重复三元组
✗ 错:命中后只移一个指针
✓ 对:命中后 l++ 且 r--
只移一个,另一指针不动会反复命中同一对
完整代码(Python / C++ / Java)
Python
def threeSum(nums):
nums.sort() # 先排序
res = []
n = len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i-1]: # 固定数去重
continue
l, r = i + 1, n - 1 # 对撞双指针
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0: l += 1 # 和小了左扩
elif s > 0: r -= 1 # 和大了右缩
else:
res.append([nums[i], nums[l], nums[r]])
l += 1; r -= 1
while l < r and nums[l] == nums[l-1]: l += 1
while l < r and nums[r] == nums[r+1]: r -= 1
return resC++
vector<vector<int>> threeSum(vector<int>& nums){
sort(nums.begin(), nums.end());
vector<vector<int>> res; int n = nums.size();
for(int i = 0; i < n - 2; i++){
if(i > 0 && nums[i] == nums[i-1]) continue;
int l = i + 1, r = n - 1;
while(l < r){
int s = nums[i] + nums[l] + nums[r];
if(s < 0) l++;
else if(s > 0) r--;
else{
res.push_back({nums[i], nums[l], nums[r]});
l++; r--;
while(l < r && nums[l] == nums[l-1]) l++;
while(l < r && nums[r] == nums[r+1]) r--;
}
}
}
return res;
}Java
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums); // 先排序
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 固定数去重
int l = i + 1, r = n - 1; // 对撞双指针
while (l < r) {
int s = nums[i] + nums[l] + nums[r];
if (s < 0) l++; // 和小了左扩
else if (s > 0) r--; // 和大了右缩
else {
res.add(Arrays.asList(nums[i], nums[l], nums[r]));
l++; r--;
while (l < r && nums[l] == nums[l - 1]) l++;
while (l < r && nums[r] == nums[r + 1]) r--;
}
}
}
return res;
}复杂度
时间
O(n²)
排序 O(n log n),外层固定 n 次 × 内层双指针线性,总 O(n²)
空间
O(1)
排序原地,只用几个指针(不计结果数组)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 三数之和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能用哈希表做到更快?+
固定一个数后,剩下用哈希找两数之和也可以,但去重更麻烦、常数更大;排序+双指针是最干净的写法,且空间 O(1)。
推广到「四数之和」怎么办?+
再套一层固定循环(固定两个数)+ 内层双指针,复杂度 O(n³),去重思路相同。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 三数之和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。