15. 三数之和
✨核心逻辑
本题采用 排序 + 双指针 的策略:
- 排序预处理:首先对数组进行排序,排序后不仅可以利用有序性快速查找到目标,还方便跳过重复元素。
- 固定第一层循环:遍历数组的每一个元素
nums[i],将其作为三个数中的第一个数。因为要求三数之和为 0,所以问题就转化为了寻找两数之和等于-nums[i]。 - 双指针寻找:在
i之后的区间[i+1, n-1]内,使用对撞指针left和right查找两个数,使它们的和等于目标值。 - 详细的去重逻辑:排序数组中可能包含大量重复元素,若不去重会导致结果中有重复的三元组,因此在遍历
i、移动left和right时都需要专门逻辑跳过重复值。
🔥代码实现(含详细变量注释)
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// n:记录数组的长度
int n = nums.length;
// 1. 先对数组进行升序排序,这是后续能够使用双指针和去重的前提
Arrays.sort(nums);
// ans:用于存放最终所有不重复的符合条件的三元组集合
List<List<Integer>> ans = new ArrayList<List<Integer>>();
// i:外层循环指针,遍历整个数组,作为三元组的第一个固定数
for (int i = 0; i < nums.length; i++) {
// 剪枝优化:因为数组已经排序,如果当前固定的 nums[i] 已经大于 0
// 那么它后面的数肯定也都大于 0,三个正数相加不可能等于 0,直接结束循环
if (nums[i] > 0) {
break;
}
// 第一层去重:如果当前固定的 nums[i] 与前一个数相同(如 -3, -3, 1, 2)
// 那么该数作为首元素的情况已经处理过,直接跳过避免结果重复
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// left:左指针,指向当前 i 之后的第一个元素
int left = i + 1;
// right:右指针,指向数组的最后一个元素
int right = n - 1;
// 当左指针在右指针左边时,继续查找
while (left < right) {
// num:计算当前三个指针指向的元素之和
int num = nums[i] + nums[left] + nums[right];
if (num == 0) {
// 如果三数之和正好为 0,将这三个元素构成 List 添加到结果集中
ans.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 (num < 0) {
// 如果三数之和小于 0,说明左侧的数太小,左指针向右移动寻找更大的数
left++;
} else {
// 如果三数之和大于 0,说明右侧的数太大,右指针向左移动寻找更小的数
right--;
}
}
}
// 循环结束后返回包含所有不重复三元组的结果集
return ans;
}
}
- ⏱️复杂度分析
时间复杂度:O(N^2),其中 N 是数组的长度。排序的时间复杂度为 O(N log N),双指针遍历部分的外层循环为 O(N),内层双指针寻找为 O(N),总体时间复杂度为 O(N^2)。
空间复杂度:O(log N) 或 O(1)。主要消耗在于排序算法所需的栈空间(Java 的 Arrays.sort 底层是双轴快排,占用 O(log N) 栈空间)。如果不计入返回值 ans 占用的空间,额外使用的指针变量为 O(1)。
🔍总结一下:
排序的目的是为了使用双指针,只前有总结过双指针的适用场景。然后定A,开双指针B,C即可