1. 题面拆解:三数之和到底在考什么
先聊个直白的感受:力扣上一万多人收藏、大厂面试出镜率极高的“三数之和”,本质上不是一道“难题”,而是一道“筛选思维惯性”的题。如果你上来就套三层for循环,那么恭喜你,样例能过,提交必超时,还会被评论区一句“暴力杯警告”扎心很久。
题面一句话就能说完:给你一个整数数组 nums,判断是否存在三个元素 a、b、c,使得 a + b + c = 0,返回所有不重复的三元组。注意限制条件有两个,第一,答案中不可以包含重复的三元组;第二,同一个元素在数组里的下标不能重复使用,但值相同的不同元素是可以同时入选的。
先说这道题在考什么。它考的从来不是“会不会写循环”,而是三件事:
- 能不能把 O(n³) 的暴力问题通过排序优化到 O(n²);
- 能不能说清楚双指针为什么能替代第三层循环;
- 能不能把“去重”做得既正确又高效,不靠集合硬去重,而是靠指针跳跃。
换句话说,它考查的是你对“无序问题的有序化处理”的能力。很多人在理解题意时就已经踩了坑——看到“不重复的三元组”,第一反应是用 HashSet 存三元组去重,但这种思路会带来两个新的麻烦:一是三元组内部顺序问题,[-1,0,1] 和 [1,0,-1] 在集合眼里是两个不同的东西,你还得先排序再存;二是额外空间复杂度变高,答案去重全靠哈希。
真正优雅的思路是:先把数组排好序,让重复值相邻排列。排序之后,“跳过重复元素”的操作就变得极其便宜——只需要比较相邻两个数是否相等。这个前置操作让后面的双指针解法水到渠成,也让你彻底摆脱了 HashSet 这种“用空间换时间”的妥协方案。
题目的边界条件也值得注意:数组长度小于 3 时,答案直接为空;全部元素都不满足组合要求时,返回空列表而不是 null。这些细节在面试中都是送分题,但恰恰是送分题最能看出一个人写代码有没有形成肌肉记忆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从暴力三重循环到双指针:一条自然的演进路线
很多刷题攻略喜欢直接甩双指针模板,然后让你背。这样做不是不行,但如果你没搞懂为什么双指针能成立,遇到变体题(比如最接近的三数之和、四数之和)照样会卡壳。所以我打算先带你走一遍从暴力解到双指针的完整推导链,这条链其实非常短。
2.1 暴力方案:三重循环为什么注定被淘汰
先看最符合直觉的暴力解法:三层循环枚举 i、j、k,判定 nums[i] + nums[j] + nums[k] == 0。这个方案的时间复杂度是 O(n³),当 n = 3000 时,理论运算量约为 270 亿次,任何在线判题系统都不可能放你通过。
有人可能想狡辩:那我可以加剪枝啊,比如固定 i 之后,j 和 k 只往后扫。但剪枝只是降低了常数因子,复杂度量级还在 O(n³),本质上依然是等死的节奏。
暴力方案的麻烦还不止性能问题。由于数组中可能存在大量重复元素,三重循环会枚举出非常多的重复三元组,比如 nums = [-1, -1, 0, 1, 1] 这种输入,暴力解会搜出多个 [-1, 0, 1],想得到唯一结果,就必须配合 HashSet 或排序去重。这就让代码的丑陋程度再上一个台阶。
所以暴力方案的真实价值只有两个:一是当作最基础的“正确性参照物”,在调试优化方案时用来对拍;二是让你切身体会到,算法优化的第一步永远是找计算冗余。
2.2 排序带来的第一个质变:重复元素相邻化
在考虑优化之前,我们先做一个只花 O(n log n) 的操作——排序。排序改变不了题目本质(寻找三元组),但它带来了两个明显的好处。
第一,重复元素变得相邻,这为后续“按值跳过重复”创造了条件。设想一下,如果数组是无序的,你想跳过值等于 nums[i] 的其他位置,必须额外遍历或使用哈希表;有序之后,只需要检查 nums[i] == nums[i-1] 就能判断当前位置的值是不是首次出现。
第二,双指针(two pointers)赖以生存的单调性前提满足了。在一个有序数组中,左指针向右移动,数值单调不减;右指针向左移动,数值单调不增。这一增一减之间,我们就能像用天平秤一样,通过移动指针精确控制三个数的总和是变大还是变小。
这个思想特别像你在一个爬坡路上找某个海拔高度:如果当前位置海拔太低,你就往高处走几步;太高了,就往低处退几步。整个过程不需要回头摸索,因为有单调性作为方向指引。
2.3 固定一个指针,让另外两个指针“对撞”
接下来是核心推导。既然要枚举三个数,那我们就固定其中一个,让另外两个用一个“对撞”的过程去搜索,这样三重循环就被降成了两重循环。
具体做法是:外层循环固定第一个数 nums[i]。既然目标是三数之和为 0,那么剩下两个数 nums[j] 与 nums[k] 的目标和就明确了,是 target = -nums[i]。此时问题退化为经典的“有序数组中找两数之和等于 target”。
在有序数组中寻找两数之和,最优解法就是双指针:一个左指针 j 从 i + 1 开始向右移动,一个右指针 k 从数组末尾开始向左移动。计算 currentSum = nums[j] + nums[k]:
- 若
currentSum == target:记录三元组(nums[i], nums[j], nums[k]),然后左指针右移、右指针左移; - 若
currentSum < target:说明两数之和偏小,只有左指针向右移动、增大nums[j],才能让总和的量级往上走; - 若
currentSum > target:说明两数之和偏大,需要右指针向左移动、减小nums[k]。
每次指针移动,都会排除掉一整批不可能产生答案的组合。举例来说,当 currentSum < target 时,意味着当前 j 和任意比 j 更靠左的指针组合都不可能满足条件,因为它们只会让值更小。双指针的 O(n) 扫描就这样替代了 O(n²) 的两层循环。
这个降维思路在整个算法题体系里都非常重要。你在“盛最多水的容器”“接雨水”里见到的双指针,和这里的思路同源——都是利用有序性缩小搜索空间。不同之处在于,这里多了一层“固定一个数”的
