力扣解题-三数之和
给定一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1: 输入:nums = [-1,0,1,2,-1,-4] 输出:[[-1,-1,2],[-1,0,1]] 解释: nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 注意,输出的顺序和三元组的顺序并不重要。
示例 2: 输入:nums = [0,1,1] 输出:[] 解释:唯一可能的三元组和不为 0 。
示例 3: 输入:nums = [0,0,0] 输出:[[0,0,0]] 解释:唯一可能的三元组和为 0 。
提示: 3 <= nums.length <= 3000 -105 <= nums[i] <= 105
Related Topics 数组 双指针 排序
第一次解答
解题思路
核心方法:排序 + 固定单值 + 双指针查找剩余两数 + 全链路去重,将暴力解法的O(n³)时间复杂度优化至O(n²),同时通过有序性实现高效的重复值过滤,确保结果无重复且性能适配题目数据规模。
具体步骤:
- 数组排序预处理:首先对输入数组
nums进行排序(Arrays.sort(nums)),核心作用有三:- 让相同元素相邻,为后续“跳过重复值”的去重逻辑提供基础;
- 使数组具备有序性,可通过双指针的单向移动快速调整三数之和的大小;
- 避免重复生成相同的三元组(如无序数组中[-1,0,1]和[0,-1,1]会被判定为不同组合,排序后则统一为[-1,0,1])。
- 外层循环固定第一个数:遍历数组下标
i(作为三元组的第一个数nums[i]),循环终止于nums.length - 2(需预留至少两个位置给后续双指针left和right):- 关键去重操作:若
i > 0且nums[i] == nums[i-1],直接跳过当前i(避免因第一个数重复生成相同三元组,如连续的-1会导致重复的[-1,0,1])。
- 关键去重操作:若
- 双指针查找剩余两数:对每个固定的
i,初始化双指针:left = i + 1:指向i的下一个位置(确保left > i,避免重复使用同一元素);right = nums.length - 1:指向数组末尾; 在left < right的条件下循环,计算三数之和sum = nums[i] + nums[left] + nums[right],并根据sum的大小调整指针:- 若
sum == 0:找到符合条件的三元组,将其添加到结果列表res;- 立即跳过
left的重复值:while (left < right && nums[left] == nums[left + 1]) left++(避免连续相同的left值生成重复三元组); - 立即跳过
right的重复值:while (left < right && nums[right] == nums[right - 1]) right--(避免连续相同的right值生成重复三元组); - 指针双向收缩:
left++和right--,继续查找当前i下的其他可能组合。
- 立即跳过
- 若
sum < 0:三数之和偏小,需增大数值,因此将左指针left右移(有序数组中left右移会使nums[left]变大); - 若
sum > 0:三数之和偏大,需减小数值,因此将右指针right左移(有序数组中right左移会使nums[right]变小)。
- 返回结果:遍历完成后,结果列表
res中存储了所有和为0且不重复的三元组,直接返回即可。
性能说明
- 时间复杂度:排序的O(nlogn) + 外层循环O(n) × 内层双指针循环O(n),整体为O(n²),适配题目
n <= 3000的规模(3000²=9×10⁶次计算,在时间阈值内); - 耗时41ms击败28.57%的用户:是因为该解法是三数之和的“标准最优解”,多数提交均采用此逻辑,耗时差异主要来自评测机环境;
- 内存消耗56.6MB击败98.98%的用户:核心原因是未使用额外的哈希表等存储结构,仅用结果列表存储最终答案,排序的空间开销为O(logn)(Java内置排序的栈空间),属于极低的内存占用。
执行耗时:41 ms,击败了28.57% 的Java用户 内存消耗:56.6 MB,击败了98.98% 的Java用户
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
// 跳过外层重复
if (i > 0 && nums[i] == nums[i - 1]) continue;
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]));
// 跳过 left 和 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)降至O(1)(单次移动);
- 去重逻辑是关键:分别对第一个数、左指针、右指针的重复值进行跳过,确保结果无重复;
- 时间复杂度O(n²)是该问题的最优下界,无法进一步优化(需至少遍历所有可能的二元组),该解法已达到理论最优性能。