LeetCode 15. 三数之和 | 排序 + 双指针超详细复盘

一、题目与考点拆解

题目要求

给定一个整数数组 nums,找出所有满足以下条件的三元组:

  1. 三个下标互不相同(i≠j、i≠k、j≠k
  2. 三数之和等于 0
  3. 答案中不能包含重复的三元组 (元素相同、顺序不同算重复,如 [-1,0,1][0,-1,1] 视为同一个)

两个核心考点

这道题真正考察的从来不是枚举能力,而是:

  1. 降维思维:如何把三数之和拆解成「固定一个数 + 两数之和」
  2. 去重能力 :如何利用有序性高效去重,避免重复结果 暴力三重循环 O (n³) 既超时又难去重,最优解的标准方案是:排序 + 双指针

二、思路推导:从暴力法到最优解

1. 暴力法:最朴素的思路

三层循环枚举所有 i<j<k 的组合,判断和是否为 0。

  • 时间复杂度:O (n³),数组长度 1000 就会超时
  • 痛点:找到的三元组存在大量重复,额外去重逻辑繁琐且易出错

2. 第一次优化:排序 + 哈希表

固定第一个数 nums[i],问题降维为:在 i 后面的区间里,找两个数之和等于 -nums[i],也就是经典的两数之和问题。

  • 用哈希表做两数之和,整体时间复杂度降到 O (n²)
  • 痛点:去重依然麻烦,需要分别处理第一个数重复、数对重复,边界条件多

3. 最终最优解:排序 + 双指针

排序是整个算法的基石,一举两得:

  1. 有序性支持双指针:数组升序后,两数之和可以用左右双指针相向查找,线性时间内完成,不需要额外哈希空间
  2. 有序性方便去重:相同元素会挨在一起,只需要简单的相邻判断就能跳过重复,优雅解决去重难题

整体时间复杂度:O(n²) (排序 O (nlogn) + 外层遍历 O (n) + 内层双指针 O (n),主导项为 n²) 空间复杂度:O(logn)(排序的栈开销,不计结果存储)


三、前置铺垫:有序数组的两数之和(双指针版)

三数之和的内核是有序数组两数之和,先把这个基础搞懂,三数之和就是套壳。

问题

给定升序数组,找出所有和为 target 的不重复数对。

双指针核心逻辑

  1. 左指针 left 指向区间起点,右指针 right 指向区间终点
  2. 计算两数之和 sum = nums[left] + nums[right]
  3. 三种情况:
    • sum == target:找到解,记录后跳过重复元素,双指针同时收缩
    • sum < target:和偏小,左指针右移,增大和
    • sum > target:和偏大,右指针左移,减小和
  4. left >= right 时停止

核心前提:数组必须有序,才有「左移和变大、右移和变小」的单调性,双指针才能成立。


四、完整算法流程(Step by Step)

  1. 边界特判:数组长度小于 3,直接返回空列表
  2. 数组升序排序:双指针和去重的基础
  3. 遍历固定第一个数 nums i
    • 提前终止:若 nums[i] > 0,后续全为正数,三数之和不可能为 0,直接终止循环
    • 首数去重:若 i>0nums[i] == nums[i-1],跳过当前轮,避免重复三元组
  4. 双指针查找剩余两数
    • 初始化:left = i+1right = nums.length - 1
    • left < right 时循环:
      • 计算当前三数之和 sum
      • sum == 0:记录结果,左右指针分别跳过重复元素,再收缩指针
      • sum < 0:左指针右移,增大和
      • sum > 0:右指针左移,减小和
  5. 返回结果集合

五、最终定稿:可直接背诵的 AC 代码

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        // 特判:长度不足3,无合法三元组
        if (nums.length < 3) return result;

        // 第一步:升序排序,双指针和去重的前提
        Arrays.sort(nums);

        // 第二步:遍历固定第一个数
        for (int i = 0; i < nums.length; i++) {
            // 首数大于0,后续全为正数,和不可能为0,提前终止
            if (nums[i] > 0) break;
            // 首数去重:和前一个相同则跳过,避免重复三元组
            if (i > 0 && nums[i] == nums[i - 1]) continue;

            // 左右双指针,在i之后的区间查找
            int left = i + 1;
            int right = nums.length - 1;

            // 第三步:双指针相向查找
            while (left < right) {
                // 每轮必须重新计算和!指针移动后和会变化
                int sum = nums[i] + nums[left] + nums[right];

                if (sum == 0) {
                    // 找到解:将三个数打包成List,加入结果集
                    result.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 result;
    }
}

六、逐行代码深度拆解(含所有疑惑点解答)

1. 结果集合与 Arrays.asList

复制代码
List<List<Integer>> result = new ArrayList<>();
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
  • result 类型是 List<List<Integer>>:大列表里装小列表,每个小列表对应一个三元组
  • Arrays.asList():Java 工具方法,作用是快速把一组元素打包成一个 List 对象,省去了「新建 ArrayList → 逐个 add」的繁琐写法,是算法题的标准语法糖
  • 注意:该方法返回的 List 固定大小,不能增删元素,但本题只用于存储结果,完全够用

2. 为什么必须先排序?

两个不可替代的作用:

  1. 赋予数组单调性,双指针的「和小左移、和大右移」逻辑才能成立
  2. 让重复元素相邻,通过简单的相邻比较就能完成去重,无需额外空间

3. nums i > 0 为什么用 break?

复制代码
if (nums[i] > 0) break;

数组是升序的,第一个数已经大于 0,后面的数都≥它,三个正数相加永远大于 0,后续所有 i 都不可能有解,因此直接终止整个外层循环,属于性能优化。

  • 换成 return result 功能上完全等价,最终结果一致;但 break 语义更精准(终止循环),属于算法题规范写法

4. 首数去重为什么用 continue?为什么和前一个比?

复制代码
if (i > 0 && nums[i] == nums[i - 1]) continue;
  • continue 的作用:只跳过当前这一轮 i,后续不重复的 i 还要继续处理,因此不能用 break
  • 和前一个比的原因 :如果和后一个比,会漏掉合法解。例如 [-1,-1,2],i=0 时若和后一个比,会直接跳过第一个 - 1,导致合法三元组被遗漏
  • 核心原则:去重跳后不跳前------ 第一个出现的元素要完整处理,后续重复出现的再跳过

5. sum 为什么必须写在 while 循环内部?

这是最高频的手写致命错误:如果把 sum 写在循环外,只会根据初始指针计算一次,后续指针移动后 sum 不会更新,所有判断全错。

记住铁则:只要指针动了,和就要重新算。sum 必须放在内层循环的第一行,每轮都更新。

6. 去重之后为什么还要额外收缩指针?

这是最容易困惑的点,核心要分清两个步骤的作用:

  1. 去重循环 :把所有和当前值相同的重复元素扫过去,指针最终停在当前值的最后一个重复位置 例如 [-1,-1,-1,0],left 初始为 0,去重循环结束后 left=2,值还是 - 1(最后一个 - 1)
  2. 额外收缩一步:从「当前值的末尾」跳到「下一个新值的开头」 上面的例子中,left++ 后变成 3,值变为 0,才真正跳到了下一个不同的元素

如果只去重不额外收缩,指针会停在旧值上,下一轮循环依然会生成重复三元组,甚至死循环。


七、复盘

坑 1:sum 写在 while 循环外(致命错误)

  • 现象:指针移动后 sum 不更新,永远基于初始值判断,找不到正确解,甚至死循环
  • 修正:sum 必须放在内层 while 循环的第一行,每轮重新计算

坑 2:首数去重和后一个元素比

  • 现象:漏掉包含重复元素的合法三元组(如 [-1,-1,2]
  • 修正:和前一个元素比,nums[i] == nums[i-1]

坑 3:去重循环不加边界判断

  • 现象:遇到全相同元素时,指针一直移动导致数组越界
  • 修正:所有去重循环都必须加上 left < right 的边界条件

坑 4:找到解后忘记收缩指针

  • 现象:指针原地不动,无限循环重复添加同一个三元组
  • 修正:去重完成后必须执行 left++right--

坑 5:循环条件写成 left <= right

  • 现象:left 和 right 指向同一个元素,出现同一个元素使用两次的情况,违反下标不同的要求
  • 修正:循环条件必须是 left < right,相遇即停止

坑 6:break 和 continue 混用

  • 现象:该终止循环的时候跳了单轮,该跳单轮的时候终止了循环
  • 修正:后续全无解用 break,仅当前元素无效用 continue

八、完整样例一步步模拟

输入:nums = [-1,0,1,2,-1,-4] 排序后:[-4, -1, -1, 0, 1, 2]

第 1 轮:i=0,nums i=-4,目标两数和 = 4

  • left=1,right=5 → sum=-4+(-1)+2=-3 < 0 → left++
  • left=2 → sum=-4+(-1)+2=-3 < 0 → left++
  • left=3 → sum=-4+0+2=-2 < 0 → left++
  • left=4 → sum=-4+1+2=-1 < 0 → left++
  • left=5,不满足 left<right → 本轮结束,无结果

第 2 轮:i=1,nums i=-1,目标两数和 = 1

  • left=2,right=5 → sum=-1+(-1)+2=0 → 记录 [-1,-1,2]
    • 左去重:nums 2=-1,nums 3=0,无重复
    • 右去重:nums 5=2,nums 4=1,无重复
    • 收缩:left=3,right=4
  • sum=-1+0+1=0 → 记录 [-1,0,1]
    • 左右均无重复
    • 收缩:left=4,right=3 → 循环结束

第 3 轮:i=2,nums i=-1

  • i>0 且 nums 2==nums 1 → 重复,continue 跳过

第 4 轮:i=3,nums i=0

  • left=4,right=5 → sum=0+1+2=3 > 0 → right--
  • left=4,不满足 left<right → 本轮结束

第 5 轮:i=4,nums i=1 > 0 → break 终止循环

最终结果:[[-1,-1,2], [-1,0,1]],与标准答案完全一致。


九、面试标准话术与延伸

口述思路(直接背)

这道题我采用排序加双指针的思路。首先对数组做升序排序,排序一方面可以让数组具备单调性,支持双指针的线性查找;另一方面可以让重复元素相邻,方便后续去重。

然后遍历数组,把每个元素作为三元组的第一个数,将三数之和问题降维成有序数组的两数之和问题。在第一个数之后的区间里,用左右双指针相向移动查找:和小于目标就左指针右移,和大于目标就右指针左移,相等就记录结果并跳过重复元素。

同时对第一个数也做去重,避免重复的三元组。整体时间复杂度是 O (n²),空间复杂度主要是排序的开销 O (logn)。

高频追问

  1. 时间复杂度为什么是 O (n²)? 排序是 O (nlogn),外层遍历 O (n),内层双指针整体也是 O (n),主导项为 n²。

  2. 四数之和怎么做? 完全一致的思路:再多套一层循环固定前两个数,剩余区间用双指针找两数之和,去重逻辑完全相同,时间复杂度 O (n³)。 通用规律:k 数之和 = k-2 层循环 + 双指针,时间复杂度 O (nᵏ⁻¹)。

  3. 能不能不用排序? 可以用哈希表,但去重逻辑非常繁琐,且需要额外空间,面试和工程中都优先用排序双指针方案。


十、复习速记口诀

先排序,再固定,降维两数双指针; 和为零,记结果,左右去重再收缩; 和小左移,和大右移,首数重复直接跳; 首数大于零,直接就停掉。


总结

三数之和是模板性极强的高频题,核心套路非常固定:排序降维 + 双指针查找 + 三处去重。 复习优先级:

  1. 先记整体框架:排序→固定首数→双指针
  2. 再记三个去重点:首数去重、左指针去重、右指针去重
  3. 最后避坑:sum 放循环内、去重要加边界、收缩指针别忘记
相关推荐
程序喵大人2 小时前
【C++进阶】STL算法与函数对象 - 02 sort为什么需要随机访问迭代器
开发语言·c++·算法
hanlin032 小时前
动态规划专练:力扣第121、122题
笔记·算法·leetcode
To_OC2 小时前
LC 74 搜索二维矩阵:换皮的二分查找,我居然一开始没看出来
javascript·算法·leetcode
文心快码BaiduComate2 小时前
文心快码荣获“中国优秀软件产品”,实力再获国家级认可
算法·代码规范
玖玥拾2 小时前
LeetCode 189 轮转数组
算法·leetcode
ZhangShao06072 小时前
题解:CF2249B
c++·算法
小玮看世界2 小时前
[Python]线段树与二分法
数据结构·算法
Tisfy5 小时前
LeetCode 3345.最小可整除数位乘积 I:暴力枚举(从n开始尝试)
数学·算法·leetcode·题解·枚举
DolphinDB智臾科技5 小时前
告别 TB 级数据搬运:DolphinDB 滤波算法库,重塑物联网数据价值链
物联网·算法