三数之和:从暴力到双指针的JS最优解拆解
在算法面试中,三数之和(3Sum)是数组类题目中的经典高频题,也是考察双指针技巧的核心题型。本文会从暴力解法的痛点切入,结合实际代码逐行拆解排序+双指针的最优解法,带你彻底掌握这道题的解题逻辑。
一、题目背景与核心要求
题目描述
给定一个包含 n 个整数的数组 nums,找出所有和为 0 且不重复的三元组 [nums[i], nums[j], nums[k]],其中 i、j、k 互不相等。
核心难点
- 避免重复的三元组(如
[-1,0,1]和[0,-1,1]视为重复); - 降低暴力解法的高时间复杂度。
二、暴力解法:思路直观但效率低下
1. 暴力思路分析
三数之和最直观的思路是三层循环枚举所有可能的三元组,判断其和是否为0,最后去重。
- 时间复杂度:O(n³),三层循环嵌套,n为数组长度,数据量稍大就会超时;
- 空间复杂度:O(1)(不考虑结果存储);
- 额外问题:需要额外处理重复三元组,增加代码复杂度。
2. 暴力解法的优化方向
暴力解法的核心问题是「重复计算」和「重复结果」,要优化需解决两个核心问题:
- 如何减少循环层数?→ 降维:固定一个数,将三数之和转为两数之和;
- 如何高效去重?→ 排序:让重复元素相邻,便于跳过。
三、最优解:排序 + 双指针
1. 核心思路拆解
最优解的思路可分为三步:
| 步骤 | 操作 | 目的 |
|---|---|---|
| 排序 | 数组升序排序(nums.sort((a,b) => a - b)) | 1. 重复元素相邻,便于去重;2. 可通过指针移动控制和的大小 |
| 固定一数 | 遍历数组,固定 nums[i] | 将三数之和转化为「找两个数,使其和为 -nums[i]」的两数之和问题 |
| 双指针找两数 | 左指针 left = i+1,右指针 right = nums.length-1 | 根据三数之和与0的大小关系移动指针,降低内层循环复杂度 |
2. 逐行解析核心代码
以下是结合注释的完整代码,并逐行拆解关键逻辑:
function threeSum(nums) {
// 1. 数组升序排序
// JS sort默认是字符串排序,必须传入(a,b)=>a-b实现数值升序
// 排序时间复杂度:O(nlogn)(JS内置sort基于快排/归并,接近快排)
nums.sort((a,b) => a - b);
const res = []; // 存储最终结果
// 2. 固定第一个数,遍历数组(i最多到nums.length-3,保证left和right有值)
for (let i = 0; i < nums.length-2; i++) {
// 跳过重复的固定数:i>0时,若当前值等于前一个,直接跳过(避免重复三元组)
// 例如:nums=[-1,-1,0,1],i=1时nums[1]=nums[0],跳过避免重复结果
if (i > 0 && nums[i] === nums[i-1]) continue;
// 3. 双指针初始化
let left = i + 1; // 左指针从固定数的下一个开始,避免重复使用同一元素
let right = nums.length - 1; // 右指针从数组末尾开始
// 4. 双指针遍历(left < right 保证指针不重叠)
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
// 找到符合条件的三元组,加入结果集
res.push([nums[i],nums[left],nums[right]]);
// 移动指针,继续寻找下一组可能的组合
left++;
right--;
// 跳过左指针重复值:当前值等于前一个,继续右移
while(left < right && nums[left] === nums[left - 1]) left++;
// 跳过右指针重复值:当前值等于后一个,继续左移
while(left < right && nums[right] === nums[right + 1]) right--;
} else if (sum < 0) {
// 和小于0,需要增大总和 → 左指针右移(升序数组,右移值更大)
left ++;
} else {
// 和大于0,需要减小总和 → 右指针左移(升序数组,左移值更小)
right --;
}
}
}
return res;
}
3. 关键细节深度解析
(1)排序的核心作用
- 去重基础:重复元素相邻,只需判断当前元素与前/后元素是否相等即可跳过;
- 指针移动依据:升序数组中,左指针右移值增大,右指针左移值减小,可精准控制和的大小。
(2)去重逻辑的三层防护
| 去重位置 | 代码逻辑 | 作用 |
|---|---|---|
| 固定数去重 | i > 0 && nums[i] === nums[i-1] | 避免同一数字多次作为三元组第一个数 |
| 左指针去重 | nums[left] === nums[left - 1] | 避免同一数字多次作为三元组第二个数 |
| 右指针去重 | nums[right] === nums[right + 1] | 避免同一数字多次作为三元组第三个数 |
(3)双指针移动的边界条件
left < right:若指针重叠,说明已遍历完所有可能的两数组合,无需继续;- 找到和为0的组合后,先移动指针再去重:避免漏解(如
[-2,0,0,2,2],先移动指针再跳过重复的0和2)。
4. 复杂度分析
- 时间复杂度:O(n²)。排序的O(nlogn) + 外层循环O(n) + 内层双指针遍历O(n),整体由O(n²)主导;
- 空间复杂度:O(logn)(排序的系统栈空间),若不考虑结果存储,可视为O(1)。
四、测试用例验证
通过典型用例验证代码正确性:
// 测试用例1:常规场景
console.log(threeSum([-1,0,1,2,-1,-4])); // [[-1,-1,2],[-1,0,1]]
// 测试用例2:全重复元素
console.log(threeSum([0,0,0,0])); // [[0,0,0]]
// 测试用例3:无符合条件的组合
console.log(threeSum([1,2,3])); // []
// 测试用例4:边界场景(数组长度不足3)
console.log(threeSum([1,2])); // []
五、同类问题举一反三
掌握三数之和的思路后,可快速解决同类问题:
- 两数之和(有序数组) :直接复用双指针思路,时间复杂度O(n);
- 最接近的三数之和:将「和等于0」改为「和最接近目标值」,记录最小差值即可;
- 四数之和:固定前两个数,双指针找后两个数,时间复杂度O(n³)。
六、总结
三数之和的最优解本质是「排序降维 + 双指针优化」:
- 排序是基础,解决了去重和指针移动的核心问题;
- 固定一数将三数问题降为两数问题,实现维度降低;
- 双指针将内层循环从O(n²)优化为O(n),大幅降低时间复杂度;
- 去重逻辑是关键,需覆盖固定数、左指针、右指针三层。
这道题的核心思想不仅适用于三数之和,更是双指针技巧的典型应用。掌握后,面对多数数组类的求和问题,都能快速找到优化方向。