最近在leetcode遇到这个题目,题目描述如下:
解决这个问题的话,我们可以借助两数之和的求解过程。两数之和的问题描述如下:
解决两数之和的话,可以使用一个hashMap,保存值与下标,类似与登记功能,比如说2这个元素来了,要凑成9的话,还需要找到7这个元素,但是发现hashMap中没有7这个元素,于是2只好等待,将自己与自己的下标存到hashMap中,之后7来了,它需要找到值为2的元素,于是它去hashMap去找,发现有2这个元素,于是目的就达成了。
代码如下:
import java.util.*;
class Solution {
public int[] twoSum(int[] nums, int target) {
int length=nums.length;
//map中存放元素和元素的索引
Map<Integer,Integer>map=new HashMap<Integer,Integer>();
int result[]=new int[2];
for(int i=0;i<length;i++){
int temp=target-nums[i];
if(map.get(temp)!=null){
result[0]=i;
result[1]=map.get(temp);
if(result[0]>result[1]){
int t=result[0];
result[0]=result[1];
result[1]=t;
}
break;
}else{
map.put(Integer.valueOf(nums[i]),Integer.valueOf(i));
}
}
return result;
}
}
下面继续说三数之和,受到两数之和的启发,我们可以将三数之和a+b+c=0,转换成b+c=-a
于是我们可以写出如下代码:
但是一点执行代码,会出现如下的提示:
可以看到,上述代码的输出结果是包含有重复结果的,且结果集中的顺序也不对。但是在上述代码中使用了Collection.sort()来对一个结果内部数据排过一次序,但是结果与结果之间的顺序是乱的,也就是说,上述代码面临着去重与结果集中结果A与结果B的顺序问题。那么如何解决这些问题呢。
我们先考虑解决结果与结果之间的顺序问题,比如上面的【-1,-1,2】应该在【-1,0,1】之前。那么思考这个问题产生的原因,可以发现,之所以会出现【-1,0,1】在【-1,-1,2】之前,是因为代码是直接从nums中顺序取元素的,且nums是未排序的,比如说题目中nums为【-1,0,1,2,-1,4】,上述代码会先对第一个-1从它后面找能够满足b+c=1的两个数,于是按照两数之和的思想,会先找到【-1,0,1】然后是【-1,-1,2】。若想使【-1,-1,2】到【-1,0,1】前面,我们首先要对nums排序
-4 -1 -1 0 1 2
然后我们就开始找吧,首先-4我们直接跳过,因为这对我们的分析不起作用,然后我们找到了-1,从第一个-1后面开始,这里好像是我们首先处理-1了,但是如果只是先对nums排序,而其余代码与之前一样的话,还是会出现一样的结果,因为当遍历到第二个-1时,hashMap中没有出现2,所以只会登记-1,这样-1就没有来得急被先处理,显然这是因为算法的问题,于是想到要改变算法,不再使用hashMap了,尽管它帮助我们解决了两数之和问题。为了确保后面的-1得到及时的处理,这里我们建立两个指针,第一个指向后面的-1,第二个指针指向nums末尾,这样做有两个好处,一是-1能够得到及时的处理,二是我们也愿意相信能使-1+c=1的数,有大概率出现在nums末尾,因为nums是有序的。
通过上面的改进,low所指向的-1能够及时的处理了。所以问题只剩下排重了。在上述的基础上,我们不难发现,如果对第一个-1找完之后,是会有【-1,-1,2】和【-1,0,1】这两个结果的,于是下标增加,指向第二个-1,如果继续对这个-1进行上述查找的话,又会得到一个重复的结果【-1,0,1】。这就出现重复的问题,可以发现,如果前后两个元素的值相等,那么应该直接跳过后面这个元素的查找。代码就可以改进为(还有一些细节处理没提及,但容易理解):
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> resultList=new ArrayList<List<Integer>>();
Arrays.sort(nums);
for(int i=0;i<nums.length-2;i++) {
if(i>0&&nums[i]==nums[i-1]){
continue;
}
int low=i+1;
int high=nums.length-1;
int mid=(0-nums[i])>>1;
while(low<high&&nums[low]<=mid&&nums[high]>=mid){
int temp=nums[low]+nums[i]+nums[high];
if(temp==0){
resultList.add(Arrays.asList(nums[i],nums[low],nums[high]));
low++;
high--;
while(low<high&&nums[low-1]==nums[low]){
low++;
}
while(low<high&&nums[high+1]==nums[high]){
high--;
}
}else if(temp>0){
high--;
}else{
low++;
}
}
}
return resultList;
}
}
上述代码中注意到有mid=(0-nums[i])>>1,显然mid是(0-a)的一半,因为nums是排过序的,而之后我们要满足b+c=-a,至少得满足b<=mid,c>=mid,如果b>mid,c也会大于mid(nums有序),这样b+c>2*mid,也就是b+c>a了。换句话说,b应该在a/2的左边,c应该在a/2的右边。我们还可以发现,如果nums[low]+nums[high]>mid时,这时应该high要右移,即high--,但是如果移动后的high与之前的high对应nums[high]相同的话,就不应该比较,因为如果相同的话,nums[low]+nums[新的high]还是会大于mid的,所以我们应向前找到一个high,这个high与之前high对应nums[high]不同。于是可以对上述代码进行如下优化
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> resultList=new ArrayList<List<Integer>>();
Arrays.sort(nums);
for(int i=0;i<nums.length-2;i++) {
//排重
if(i>0&&nums[i]==nums[i-1]){
continue;
}
int low=i+1;
int high=nums.length-1;
int mid=(0-nums[i])>>1;
while(low<high&&nums[low]<=mid&&nums[high]>=mid){
int temp=nums[low]+nums[i]+nums[high];
if(temp==0){
resultList.add(Arrays.asList(nums[i],nums[low],nums[high]));
low++;
//排重
while(low<high&&nums[low-1]==nums[low]){
low++;
}
high--;
//排重
while(low<high&&nums[high+1]==nums[high]){
high--;
}
if(nums[high]<mid){
break;
}
}else if(temp>0){
high--;
//这里不是对正确结果排重
//移动后的nums[high]与移动前的nums[high]相同话,结果肯定是会不满足的
//所以要找到与之前不同的nums[high]
while(low<high&&nums[high+1]==nums[high]){
--high;
}
}else{
low++;
//这里不是对正确结果排重
//移动后的nums[low]与移动前的nums[low]相同话,结果肯定是会不满足的
//所以要找到与之前不同的nums[low]
while(low<high&&nums[low]==nums[low-1]){
++low;
}
}
}
}
return resultList;
}
}