在LeetCode数组类题目中,"三数之和"是经典的中等难度题,核心考点在于去重逻辑 与双指针优化。本题不仅要求找到所有和为0的三元组,还需保证结果无重复,直接暴力枚举会因时间复杂度过高和重复结果问题无法通过测试。本文将详细拆解排序+双指针的最优解法,逐行解析代码逻辑,帮你彻底搞懂这道题的解题关键。
一、题目回顾
给定一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足:
-
下标互不相等(
i != j、i != k、j != k) -
三数之和为0(
nums[i] + nums[j] + nums[k] == 0)
返回所有满足条件且不重复的三元组。注意:答案中不可包含重复三元组,顺序无关紧要。
二、解题思路:为什么选择"排序+双指针"?
首先分析暴力解法的问题:直接三重循环枚举所有三元组,时间复杂度为 O(n³),对于 n=3000 的测试用例(本题常见规模),会严重超时;同时需要额外处理大量重复三元组,代码复杂度极高。
而"排序+双指针"解法可将时间复杂度优化至 O(n²),且能天然适配去重逻辑,核心思路如下:
-
排序预处理:先对数组排序,一方面可通过有序性优化双指针移动逻辑,另一方面能快速跳过重复元素实现去重;
-
固定单指针 :遍历数组,将当前元素作为三元组的第一个元素
nums[i],转化为"在剩余元素中找两个数,使其和为-nums[i]"的两数之和问题; -
双指针找目标 :用左指针
left = i+1(剩余元素起始)、右指针right = n-1(剩余元素末尾),根据三数之和调整指针位置,找到符合条件的三元组; -
多层去重:对固定的第一个元素、左指针、右指针分别做去重处理,避免生成重复三元组。
三、完整代码与逐行解析
以下是TypeScript实现的最优解法,代码结构清晰,包含关键注释,我们逐段拆解核心逻辑:
typescript
function threeSum(nums: number[]): number[][] {
const res: number[][] = [];
const n = nums.length;
// 边界条件:数组长度小于3,不可能组成三元组,直接返回空数组
if (n < 3) return res;
// 1. 排序(核心前提:为双指针移动和去重提供基础)
nums.sort((a, b) => a - b);
// 2. 固定第一个数,遍历数组作为三元组的第一个元素nums[i]
for (let i = 0; i < n; i++) {
// 去重1:当前数与前一个数相同,跳过(避免重复起始元素导致重复三元组)
if (i > 0 && nums[i] === nums[i - 1]) continue;
// 优化:排序后若当前数>0,后续所有数都>=它,三数之和必>0,直接退出循环
if (nums[i] > 0) break;
// 双指针初始化:左指针从i+1开始(避免重复使用nums[i]),右指针从数组末尾开始
let left = i + 1;
let right = n - 1;
// 双指针遍历剩余元素
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
// 找到符合条件的三元组,加入结果集
res.push([nums[i], nums[left], nums[right]]);
// 去重2:左指针跳过重复值(避免同一nums[i]下,左指针重复导致重复三元组)
while (left < right && nums[left] === nums[left + 1]) left++;
// 去重3:右指针跳过重复值(同理,避免右指针重复)
while (left < right && nums[right] === nums[right - 1]) right--;
// 移动指针,寻找下一组可能的三元组
left++;
right--;
} else if (sum < 0) {
// 和偏小:左指针右移,增大数值(排序后左指针右移数值递增)
left++;
} else {
// 和偏大:右指针左移,减小数值(排序后右指针左移数值递减)
right--;
}
}
}
return res;
}
核心逻辑拆解
1. 边界条件处理
当数组长度 n < 3 时,无法组成三元组,直接返回空数组,这是最基础的剪枝操作,避免无效遍历。
2. 排序的关键作用
排序不仅能让双指针根据和的大小调整位置(和小移左、和大移右),更重要的是为去重提供了便利------重复元素会相邻排列,只需跳过相邻重复值即可避免重复三元组。注意排序函数需写全 (a, b) => a - b,否则会按字符串字典序排序,导致结果错误。
3. 固定元素的去重与优化
-
去重逻辑 :
if (i > 0 && nums[i] === nums[i - 1]) continue。若当前元素与前一个元素相同,说明已以该元素为起始生成过三元组,跳过可避免重复(例如数组[-1,-1,2],i=1时与i=0元素相同,跳过i=1)。 -
剪枝优化 :
if (nums[i] > 0) break。排序后数组递增,若当前元素已大于0,后续元素均≥它,三数之和必大于0,无需继续遍历,直接退出循环,大幅提升效率。
4. 双指针的移动与去重
双指针遍历剩余元素时,根据三数之和 sum 的大小调整位置:
-
sum === 0:找到目标三元组,加入结果集。之后需跳过左右指针的相邻重复值(例如左指针下一个元素与当前相同,右移左指针),再移动双指针寻找下一组; -
sum < 0:和偏小,左指针右移(增大数值),使和向0靠近; -
sum > 0:和偏大,右指针左移(减小数值),使和向0靠近。
注意:双指针的去重需在找到目标三元组后执行,若提前去重可能跳过有效组合。
四、常见问题与避坑指南
1. 去重逻辑遗漏导致重复三元组
这是本题最容易出错的点,需保证三层去重:固定元素去重、左指针去重、右指针去重。例如未处理左指针重复,会导致 [-1,0,1,-1,2] 生成多个 [-1,0,1]。
2. 双指针初始化错误
左指针必须从 i+1 开始,而非0,否则会重复使用固定元素(导致下标重复或三元组重复)。
3. 排序后直接暴力枚举
排序后若仍用三重循环,时间复杂度仍为 O(n³),无法通过大测试用例,双指针是降低时间复杂度的核心。
4. 忽略"和为0但元素重复"的特殊场景
例如数组 [0,0,0],需正确处理固定元素和双指针的重复,最终返回 [[0,0,0]],而非空数组或多个重复三元组。
五、测试用例验证
我们用几个典型测试用例验证代码正确性:
typescript
// 测试用例1:常规场景
console.log(threeSum([-1,0,1,2,-1,-4]));
// 输出:[[-1,-1,2],[-1,0,1]]
// 测试用例2:边界场景(空数组/长度不足3)
console.log(threeSum([])); // 输出:[]
console.log(threeSum([0])); // 输出:[]
// 测试用例3:全零场景
console.log(threeSum([0,0,0])); // 输出:[[0,0,0]]
// 测试用例4:含重复元素场景
console.log(threeSum([-2,0,0,2,2])); // 输出:[[-2,0,2]]
六、复杂度分析
-
时间复杂度 :
O(n²)。排序时间为O(n log n),外层循环遍历固定元素为O(n),内层双指针遍历为O(n),整体主导项为O(n²); -
空间复杂度 :
O(1)(不计算结果存储)。排序使用的空间取决于语言实现(JavaScript/TypeScript的sort为混合排序,空间复杂度可视为O(1)),双指针仅使用常数额外空间。
七、总结
LeetCode 15. 三数之和的核心是"排序+双指针"的组合策略,通过排序解决去重和指针移动逻辑问题,通过双指针将三重循环优化为二重循环,实现时间复杂度的降级。解题的关键在于精准把控三层去重逻辑 和双指针的移动规则,同时通过剪枝操作进一步提升效率。
这类"多数之和"问题有通用解题思路:两数之和用哈希表,三数之和/四数之和用"排序+双指针",本质是通过预处理和指针优化降低时间复杂度。掌握本题后,可类比解决 LeetCode 18. 四数之和等同类题目。