← 返回题目列表

三数之和怎么做?为什么先排序再用双指针?如何去重?

高频 中等 第 5 / 27 题 更新于 2026/07/28
双指针对撞指针三数之和去重

简化版

找出数组里所有和为 0 的三元组。做法:先排序,然后固定一个数 nums[i],在它右边用对撞指针找两数之和 = -nums[i]。排序把 O(n³) 的暴力枚举降到 O(n²)(外层固定 i 是 O(n),内层双指针 O(n))。去重是难点:要跳过重复的 i、以及找到一组解后跳过重复的 left/right,避免输出重复三元组。

详细版

List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);                       // 1. 排序
    List<List<Integer>> res = new ArrayList<>();
    for (int i = 0; i < nums.length - 2; i++) {
        if (nums[i] > 0) break;              // 最小的都 > 0,不可能凑出 0
        if (i > 0 && nums[i] == nums[i-1]) continue; // 跳过重复的 i
        int left = i + 1, right = nums.length - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum == 0) {
                res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                while (left < right && nums[left] == nums[left+1]) left++;   // 跳重复
                while (left < right && nums[right] == nums[right-1]) right--; // 跳重复
                left++; right--;
            } else if (sum < 0) left++;      // 和太小,左指针右移
            else right--;                     // 和太大,右指针左移
        }
    }
    return res;
}

排序 O(n log n) + 主体 O(n²) = O(n²);去重靠三处「跳过相邻相等」。

完整版教学

一、为什么先排序

暴力解法是三重循环枚举所有三元组,O(n³)。排序带来两个关键好处:

  1. 能用对撞指针:排序后数组有序,固定一个数后,剩下「找两数之和等于某值」就能用 O(n) 的对撞指针(而不是 O(n²) 的嵌套或哈希),把总复杂度降到 O(n²)。
  2. 方便去重:排序后相同的数会相邻,去重只需「跳过和前一个相等的元素」,非常简单。如果不排序,去重要用 Set 存三元组,麻烦且慢。

所以「排序 + 对撞指针」是三数之和的标准框架。

二、核心结构:固定一个 + 双指针找两个

把三数之和 a + b + c = 0 转化为:固定 a = nums[i],在 i 右边的有序区间里找 b + c = -a。这正是「有序数组两数之和」——用对撞指针 leftright:

  • sum < 0 → 需要更大的和 → left++
  • sum > 0 → 需要更小的和 → right--
  • sum == 0 → 记录这组解,然后两个指针都向内移动继续找。

外层 O(n) 遍历固定数,内层 O(n) 双指针,总 O(n²)。

三、去重的三个地方(重点,最易错)

三数之和最容易出错的就是去重——要保证输出的三元组不重复。三个地方都要处理:

  1. 固定数 i 去重:if (i > 0 && nums[i] == nums[i-1]) continue;——如果当前 nums[i] 和上一个相同,它能组成的三元组上一轮已经找过了,跳过。注意是和 i-1 比(前一个),不是 i+1,否则会漏掉合法的重复(如 [0,0,0])。
  2. 找到解后 left 去重:while (nums[left] == nums[left+1]) left++;——跳过和当前 left 相同的,避免重复三元组。
  3. 找到解后 right 去重:while (nums[right] == nums[right-1]) right--;——同理跳过重复的 right。

只有三处都去重,才能保证结果无重复。这是本题的核心考点。

四、剪枝优化

排序后可以加剪枝加速:

  • if (nums[i] > 0) break;:数组已排序,如果最小的固定数 nums[i] 都大于 0,那三个正数之和不可能是 0,后面更大,直接结束。
  • 类似地,可以判断 nums[i] + nums[i+1] + nums[i+2] > 0 直接 break、nums[i] + nums[n-1] + nums[n-2] < 0continue,进一步剪枝。

剪枝必须建立在升序数组上,并且只能排除整段不可能产生 0 的候选。第一条最小和已经大于 0 时后续只会更大;第二条最大和仍小于 0 时当前 i 无解,但后续更大的 i 仍可能有解,所以只能 continue,不能 break

五、走一个例子

nums = [-1, 0, 1, 2, -1, -4]
排序后: [-4, -1, -1, 0, 1, 2]
i=0(-4): left,right 找和=4,无解
i=1(-1): 找和=1 → (-1,0,1)? left=2(-1)... 找到 (-1,-1,2) 和 (-1,0,1)
i=2(-1): 和 i=1 相同 → 跳过(去重)
结果: [[-1,-1,2], [-1,0,1]]

六、复杂度与延伸

  • 时间 O(n²):排序 O(n log n) + 主体 O(n²)。
  • 空间 O(1)(不算结果和排序栈)。
  • 延伸:
    • 四数之和:再套一层固定,变成固定两个 + 双指针,O(n³)。
    • 最接近的三数之和:同样框架,记录最接近 target 的和。
    • 三数之和 = target(非 0):把 -nums[i] 换成 target - nums[i]

七、把不变量、推演与工程边界落到代码上

算法正确性的核心不是记住某个 while,而是始终维护这个不变量:排序后固定 i,左右指针在剩余有序区间中只排除不可能的和。

对应的状态推进是:sum<0 左移,sum>0 右移,命中后三处都要跳过重复值。每次推进前都应能解释“为什么被排除的部分不可能再包含答案”,否则只是碰巧通过样例。

初始化边界与状态
while 尚未结束:
    根据当前状态作出唯一可证明安全的选择
    更新边界、计数或局部结构
    断言不变量仍然成立
返回不变量在终止状态下推出的答案

复杂度不能只背一个符号。排序 O(n log n),外层 n 次线性夹逼使总时间 O(n²)。

带数字走一遍:[-1,0,1,2,-1,-4] 排序后得到 [-1,-1,0,1,2] 与 [-1,0,1]。手算时要同时记录下标、区间含义和本轮排除的候选,才能暴露等号与边界错误。

核对项结论
前提输出通常要求不重复三元组;值和可能需要 long 防溢出
时间复杂度O(n²)
额外空间除排序栈外 O(1),不计结果
关键边界i 与命中后的 left/right 都要去重;i>0 且 a[i]>0 可提前结束
替代方案两数之和有序版是其内层;k-sum 可递归降维

易错点:模板只有在前提和不变量一致时才成立;换了区间定义、重复值规则或输出语义,等号与推进方式就必须重新推导。

实现完成后至少检查五类用例:

  • 空输入或题目允许的最小规模,验证初始化不会越界。
  • 单元素与两个元素,验证循环条件和最后一次推进。
  • 大量重复值,验证相等分支、稳定性或去重语义。
  • 已满足目标性质与极端逆序/偏斜分布,观察最好和退化路径。
  • 上面的数字样例逐轮手算,并与程序记录的状态轨迹逐项对照。

八、常见误区与追问

  • 误区:只要记住模板就适用于所有输入。 本题成立的前提是“输出通常要求不重复三元组;值和可能需要 long 防溢出”,前提被破坏后必须换算法或重新证明。
  • 误区:复杂度只写 O(n²) 就算完整。 还要说明输入参数、预处理、递归栈以及最坏退化条件;本题的推导是“排序 O(n log n),外层 n 次线性夹逼使总时间 O(n²)”。
  • 误区:重复值和边界值不会改变代码。 i 与命中后的 left/right 都要去重;i>0 且 a[i]>0 可提前结束。
  • 追问:为什么每次推进不会漏掉答案? 因为始终维护“排序后固定 i,左右指针在剩余有序区间中只排除不可能的和”,被舍弃区域已由顺序或状态关系证明不可能更优。
  • 追问:用一个数字例子怎么讲? 可以从“[-1,0,1,2,-1,-4] 排序后得到 [-1,-1,0,1,2] 与 [-1,0,1]”开始,逐轮写出状态与被排除区间。
  • 追问:什么时候不该使用这个方案? 当前提不满足或退化代价不可接受时,应考虑“两数之和有序版是其内层;k-sum 可递归降维”。
  • 追问:如何设计自测避免只过样例? 同时覆盖最小规模、重复值、极端分布、无解/边界解,并对关键状态加断言。

九、加强记忆

三数之和:先排序,再固定 nums[i]、在右边用对撞指针找两数之和 = -nums[i],O(n²)(排序让暴力 O(n³) 降一阶,还方便去重)。去重是核心,三处都要:固定数和 i-1 比跳过、找到解后 left/right 各跳过相邻相等。剪枝:nums[i]>0 直接 break。延伸到四数之和(再固定一层)、最接近三数之和(记录最接近)。