题目描述
思路解析
一句话答案: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],少一处就会重复。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
记住这条「排序 + 固定一个 + 双指针找两数」,下面每帧都在套它。
排序后的数组:[-4,-2,-1,-1,0,1,2,3]。从左到右依次固定第一个数,剩下区间用双指针对撞。
固定第一个数 nums[0]=-4,于是要在它右边找「两数之和 = 4」(也就是 0 减去它)。
双指针就位:左指针 l 指向 1(值 -2),右指针 r 指向末尾 7(值 3),当前两数和 1,目标 4。
两数和比目标 4 小,需要更大的数 → 左指针右扩到 2(值 -1),现在两数和 2。
两数和比目标 4 小,需要更大的数 → 左指针右扩到 3(值 -1),现在两数和 2。
两数和比目标 4 小,需要更大的数 → 左指针右扩到 4(值 0),现在两数和 3。
两数和比目标 4 小,需要更大的数 → 左指针右扩到 5(值 1),现在两数和 4。
命中!-4 + 1 + 3 = 0,收下三元组 [-4,1,3](绿色高亮)。然后两指针同时内移继续找。
固定第一个数 nums[1]=-2,于是要在它右边找「两数之和 = 2」(也就是 0 减去它)。
双指针就位:左指针 l 指向 2(值 -1),右指针 r 指向末尾 7(值 3),当前两数和 2,目标 2。
命中!-2 + -1 + 3 = 0,收下三元组 [-2,-1,3](绿色高亮)。然后两指针同时内移继续找。
命中后跳过和刚才相同的相邻值(l 跳到 4、r 跳到 6),避免收到重复三元组(内层去重)。
命中!-2 + 0 + 2 = 0,收下三元组 [-2,0,2](绿色高亮)。然后两指针同时内移继续找。
固定第一个数 nums[2]=-1,于是要在它右边找「两数之和 = 1」(也就是 0 减去它)。
双指针就位:左指针 l 指向 3(值 -1),右指针 r 指向末尾 7(值 3),当前两数和 2,目标 1。
两数和比目标 1 大,需要更小的数 → 右指针左缩到 6(值 2),现在两数和 1。
命中!-1 + -1 + 2 = 0,收下三元组 [-1,-1,2](绿色高亮)。然后两指针同时内移继续找。
命中!-1 + 0 + 1 = 0,收下三元组 [-1,0,1](绿色高亮)。然后两指针同时内移继续找。
nums[3]=-1 和前一个固定数相同,再固定一遍会得到重复三元组,直接跳过(外层去重)。
固定第一个数 nums[4]=0,于是要在它右边找「两数之和 = 0」(也就是 0 减去它)。
双指针就位:左指针 l 指向 5(值 1),右指针 r 指向末尾 7(值 3),当前两数和 4,目标 0。
两数和比目标 0 大,需要更小的数 → 右指针左缩到 6(值 2),现在两数和 3。
两数和比目标 0 大,需要更小的数 → 右指针左缩到 5(值 1),现在两数和 2。
固定第一个数 nums[5]=1,于是要在它右边找「两数之和 = -1」(也就是 0 减去它)。
双指针就位:左指针 l 指向 6(值 2),右指针 r 指向末尾 7(值 3),当前两数和 5,目标 -1。
两数和比目标 -1 大,需要更小的数 → 右指针左缩到 6(值 2),现在两数和 4。
整趟扫完,所有和为 0 的不重复三元组:[-4,1,3],[-2,-1,3],[-2,0,2],[-1,-1,2],[-1,0,1]。排序 O(n log n) + 外层固定 × 内层双指针 O(n²)。
边界先想清:全是 0、全正数、含相邻重复值。
两个高频追问,串起「N 数之和」这一类。
参考代码
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 res复杂度
- 时间:O(n²),排序 O(n log n),外层固定 n 次 × 内层双指针线性,总 O(n²)
- 空间:O(1),排序原地,只用几个指针(不计结果数组)
易错点
面试追问把动画讲成自己的话
追问能不能用哈希表做到更快?
追问推广到「四数之和」怎么办?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
盛最多水的容器
LeetCode 11 · 中等 · 沿着 双指针 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题