题目描述
给你一个包含n个整数的数组nums,给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组
注意:答案中不可以包含重复的三元组
提示:数组 双指针
示例
输入:[-1, 0, 1, 2, -1, -4]
输出:[[-1, 0, 1], [-1, -1, 2]]
题解
刚开始在做的时候看到方法的返回值是一个List集合,而List集合不是有序的吗?这样怎么确保返回的三元组的次序和答案中的是一样的呢?接着就想先发数组排个序看看能不能有什么结果,模拟了一遍之后发现示例给出的三元组的次序是和排完序之后元素的顺序一样的。就继续往这个思路下去了(当然一开始想到的就是暴力的算法咯~)
但是排完序有什么用呢?这个时候就看到题目给的提示了:“双指针”。嗯哼,既然题目要求是三个元素相加,数组遍历肯定是要遍历的,但是怎么样遍历顺序更快呢?因此可以这样:每次循环选中一个元素,用双指针来指向另外两个元素,选中的元素固定不变,指针用来移动。
具体思路
用示例给出的例子举例:
左右指针用i, j 来表示,选中元素的下标用k来表示, 数组的长度用len表示
数组排完序后是: [-4, -1, -1, 0, 1, 2]
-
选中第一个元素,即 k = 0; i = k + 1 = 1; j = len - 1;即让每选中时,左指针为当前选中元素的下标+1,右指针为数组最后一个元素的下标。
sum = -4 - 1 + 2 = - 3 < 0
观察每次计算的三个元素的和,由于数组是排好序的,
- 如果这三个元素的和小于0,那么自然的为了让和的值变大(尽可能往0方向靠),因此需要将左指针右移,即 i++;
- 如果这三个元素的和大于0, 那么自然为让和的值变小,因此需要将右指针左移,即 j–;
- 如何这三个值刚好为0,那么将这三个值记录,并且继续移动,看能不能继续找到满足条件的值。当然这里要注意可能存在重复答案的情况。
重复上述过程,当左指针等于右指针时退出循环。
最终这次遍历没有找到符合条件的情况!
-
选中第二个元素,即k = 1; i = k + 1 = 2; j = len - 1; 执行过程同上,最终得到的结果为 [-1, -1, 2]
-
选中第三个元素, 即k = 2; i = k +1 = 3; j = len - 1; 最终得到的结果为[-1, 0, 1]
-
选中第四个元素,即k = 3; 此时nums[k] = 0;没有结果符合条件
-
选中第五个元素,即k = 4; 此时nums[k] = 1; 可以见得,在数组排好序的情况下,后面不可能再有三个元素相加等于0的情况出现了。因此直接退出循环即可。
程序实现
代码是用java实现的,不过程序的框架应该都是差不多的,只是选用的容器不太一样罢了。选中元素可以使用for循环来,因为遍历的次数相对确定。两个指针的移动可以使用while循环来,因为退出循环的情况得看情况而定。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length <= 2) {
//数组元素少于3个,没有结果,直接返回
return result;
}
Arrays.sort(nums); //数组排序
Set<List<Integer>> set = new HashSet<>(); //定义set容器,来排除可能存在重复的结果
for (int k = 0; k < nums.length; k++) {
int i = k + 1, j = nums.length - 1;
if (nums[k] > 0) {
//选中的元素大于0的,后面不可能出现满足条件的结果了,循环直接退出
break;
}
if (k == 0 || nums[k] != nums[k - 1]) {
while (i < j) {
int sum = nums[k] + nums[i] + nums[j];
if (sum > 0) {
j--;
} else if (sum < 0){
i++;
} else {
set.add(Arrays.asList(nums[k], nums[i], nums[j])); //符合条件添加元素
i++;
j--;
}
}
}
}
result = new ArrayList<>(set); // 将set集合转化为List集合 返回
return result;
}
}
程序执行结果
出奇的慢~,看了一下别人写的题解,思路上好像差不多。但是发现它们并不是用set集合来排除重复的元素的,而是直接在程序中排除的。照着它们的思路不用set集合后的程序和提交结果如下:
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums.length <= 2) {
return result;
}
Arrays.sort(nums);
//Set<List<Integer>> set = new HashSet<>(); //定义set容器,来排除可能存在重复的结果
for (int k = 0; k < nums.length; k++) {
int i = k + 1, j = nums.length - 1;
if (nums[k] > 0) {
break;
}
if (k == 0 || nums[k] != nums[k - 1]) {
while (i < j) {
int sum = nums[k] + nums[i] + nums[j];
if (sum > 0) {
j--;
} else if (sum < 0){
i++;
} else {
//set.add(Arrays.asList(nums[k], nums[i], nums[j])); //符合条件添加元素
result.add(Arrays.asList(nums[k], nums[i], nums[j])); // 直接添加到返回的list集合中
while (i < j && nums[i] == nums[i + 1]) i++; // 排除重复
while (i < j && nums[j] == nums[j - 1]) j--; //排除重复
i++;
j--;
}
}
}
}
//result = new ArrayList<>(set); // 将set集合转化为List集合 返回
return result;
}
}
可见比使用set集合快了很多,其原因好像是因为在最终的返回结果时的
result = new ArrayList<>(set);
该构造方法中有个Array.copy用来复制元素的,导致程序慢了许多~