15. 3Sum (medium)
描述
Given an array nums
of n integers, are there elements a, b, c in nums
such that a + b + c = 0? Find all unique triplets in the array which gives the sum of zero.
Note:
The solution set must not contain duplicate triplets.
Example:
Given array nums = [-1, 0, 1, 2, -1, -4],
A solution set is:
[
[-1, 0, 1],
[-1, -1, 2]
]
分析
首先是最容易想到的暴力破解,通过三重遍历数组nums
,依次确定nums[i]
,nums[j]
,nums[k]
并计算三元素之和是否为0。这是最粗暴的解法,但是存在去重的问题,如[-1, 0, 1]和[0, 1, -1]这种情况。
其次,这个题目作为2Sum的进阶题目,很容易联想到将3Sum转化为求target值为0 - nums[i]
,并在数组剩余元素中找出两个元素之和为target的2Sum问题。但是同样存在去重问题。
关于去重,由于是一个List对象,不方便使用Set和Map。解决方法可以为对List<List>的每个item(List)进行排序,然后针对要add的item,和结果集中的所有item比较是否相等。因为都进行了排序操作,所以同时比较item.get(0)
,item.get(1)
和item.get(2)
三者相等即为重复,拒绝添加进结果集。
最后,以上两种思路都存在去重问题,问题需要的是找出数组中三个元素之和为0的所有组合。去重过程很明显是和结果无关,但是却非常麻烦,因此要优化算法就要着眼于移除去重这个步骤。
在暴力破解的时候就该意识到边界问题。
在做第一层循环时可以这样写:for(int i = 0; i < nums.length - 2; i++)
。即第一层循环的结束条件是nums.length - 2
,并不需要到nums.length
。
同样第二层循环时:for(int j = i + 1; j < nums.length - 1; j++)
。开始索引不需要从0开始,可以直接从i + 1
开始,而结束为nums.length - 1
。
第三层:for(int k = j + 1; k < nums.length; k++)
。
可以看到在暴力破解的时候,我们已经有意识地通过边界条件过滤掉一些情况,进行了初步优化。注意到数组本身是无序的,所以在确定元素的时候难以界定当前遍历元素是否已经被选中过。如果数组是有序的,那么三重遍历的时候就可以有意识地跳过重复元素。到这里已经对暴力破解的解题思路进行了优化,但是三重遍历无疑是导致时间复杂度为O(N^3),这么高的时间复杂度肯定是要被抛弃的。那么该如何继续优化呢?
尝试优化思路二。首先使用排序解决去重问题。遍历排序后的数组,固定第一个元素为nums[i]
,接下来在索引位i + 1
至nums.length
之间找出两个元素nums[j]
和nums[k]
,二者之和为0 - nums[i]
。固然这可以做遍历两次达到目的,相信基本上2Sum都是这样完成的。但是针对一个有序数组,夹逼法可以将这个过程的时间复杂度降为O(N)。因此,使用夹逼法找出剩余两个元素。ps,别忘了同时对2Sum使用夹逼法进行优化。
代码
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new LinkedList<List<Integer>>();
if (nums == null || nums.length < 3) {
return result;
}
// 对数组排序
Arrays.sort(nums);
//固定第一个元素nums[i]
for (int i = 0; i < nums.length - 2; i++) {
//默认是从小到大的排序,所以当nums[i]大于0的时候,就可以结束
if (nums[i] > 0) {
break;
}
//nums[i - 1] != nums[i]执行了去重,注意这里在理解的时候要意识到此时操作的数组已经是有序数组
if (i == 0 || nums[i - 1] != nums[i]) {
//使用加逼法
int j = i + 1;
int k = nums.length - 1;
while (j < k) {
int sum = nums[i] + nums[j] + nums[k];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[j], nums[k]));
}
if (sum <= 0) {
while (j < k && nums[j] == nums[++j]);
}
if(sum >= 0){
while (j < k && nums[k] == nums[--k]);
}
}
}
}
return result;
}
上面的代码是优化之后的代码,对于理解加逼的过程有点不便,下面是加逼的原始写法:
while (j < k) {
int target = 0 - nums[i];
if(target == (nums[j] + nums[k])){
result.add(Arrays.asList(nums[i], nums[j], nums[k]));
j++;
while(nums[j] == nums[j - 1] && j < k){ //去重,注意这是一个有序数组
j++;
}
k--;
while(nums[k] == nums[k + 1] && j < k){ //去重,注意这是一个有序数组
k--;
}
}else if(target < (nums[j] + nums[k])){
k--;
while(nums[k] == nums[k + 1] && j < k){
k--;
}
}else if(target > (nums[j] + nums[k])){
j++;
while(nums[j] == nums[j - 1] && j < k){
j++;
}
}
}