2.2.1 题目说明
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
尝试:第一种暴力法
public List<List<Integer>> threeSum1(int[] nums){
int n = nums.length;
// 定义结果列表
List<List<Integer>> result = new ArrayList<>();
// 三重for循环,枚举所有的三数组合
for (int i = 0; i < n - 2; i++){
for (int j = i + 1; j < n - 1; j++){
for (int k = j + 1; k < n; k++){
if (nums[i] + nums[j] + nums[k] == 0)
result.add( Arrays.asList(nums[i], nums[j], nums[k]) );
}
}
}
return result;
}
此种解法时间复杂度是o(n^3),空间复杂度为O(n),并且答案也无法满足题目找出不重复的三元组,这个结果是不去重的,同样的三元组在结果中无法排除.比如-1,0,1会出现两次。而且时间复杂度非常高是O(N)的三次方,接下来试图做些改进,降低复杂度,而且解决去重问题
尝试2:运用哈希表(要做去重,首先想到就是把结果保存到hash表里。仿照两数之和,直接存到HashMap中查找)
public List<List<Integer>> threeSum2(int[] nums){
int n = nums.length;
List<List<Integer>> result = new ArrayList<>();
HashMap<Integer,List<Integer>> map = new HashMap<>();
for(int i =0;i<n;i++){
int thatNum = 0 - nums[i];
//如果已经存在了thatNum,就找到了一组解
if(map.containsKey(thatNum)){
ArrayList<Integer> list = new ArrayList<>(map.get(thatNum));
list.add(nums[i]);
result.add(list);
}
//如果没找到,就把当前两个值添加到map
for(int j = 0;j < i;j++) {
int newKey = nums[i] + nums[j];
//如果key不存在,就直接添加进去
if (!map.containsKey(newKey)) {
ArrayList<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[j]);
map.put(newKey, list);
}
}
}
return result;
}
因为尽管通过HashMap存储可以去掉相同二元组的计算结果的值,但没有去掉重复的输出(三元组)。这就导致,0对应在HashMap中有一个值(0,List(0,0)),第三个0来了会输出一次,第四个0来了又会输出一次。如果希望解决这个问题,那就需要继续加入其它的判断来做去重,整个代码复杂度会变得更高。可以尝试用set去解一下,不过思维打开了,把复杂度降下去了,为O(n^2),最后经过我的思考,我终于发现了一个非常好的解法
解法:双指针法
public List<List<Integer>> threeSum3(int[] nums){
int n = nums.length;
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);//-4 -1 -1 0 1 2
//1.遍历每个元素,找到三个数的最小的那个作为核心
for(int i = 0 ;i < n;i++){
// 1.1 如果当前数已经大于0,直接退出循环
if (nums[i] > 0 ) break;
// 1.2 如果当前数据已经出现过,直接跳过
if ( i > 0 && nums[i] == nums[i-1] ) continue;
// 1.3 常规情况,以当前数做最小数,定义左右指针
int lp = i + 1;
int rp = n - 1;
//当左右指针遇到时,结束循环
while(lp < rp){
int sum = nums[i] + nums[lp] + nums[rp];
if (sum == 0){
// 1.3.1 等于0,就是找到了一组解
result.add(Arrays.asList(nums[i], nums[lp], nums[rp]));
lp ++;
rp --;
// 如果移动之后的元素相同,直接跳过
while (lp < rp && nums[lp] == nums[lp - 1]) lp++;
while (lp < rp && nums[rp] == nums[rp + 1]) rp--;
}
// 1.3.2 小于0,较小的数增大,左指针右移
else if(sum < 0)
lp ++;
// 1.3.3 大于0,较大的数减小,右指针左移
else
rp --;
}
}
return result;
}
时间复杂度为O(n^2),比暴力法有了很大的改善,空间复杂度仅为O(1),可以说双指针一种很巧妙非常优雅的设计.
最后,关于第一个暴力法我是在是想不出有什么办法可以去重,如果你们有什么好的想法,欢迎来讨论.至此结束,别忘记关注哦,算法日常更新思路.