思路: 遍历+双指针
四数之和是三数之和的升级版,思路一脉相承,大体是通过遍历依次控制住数组中的每个元素,在每次循环过程中使用双指针来对整个数组的某几位数进行相加之和的检索判断。只不过四数之和比三数之和要多一个循环,可以认为是在三数之和循环的基础上外套了一层循环来实现四个数相加之和的实现。但是编程的实现往往才是难点,细节主要有两点,为了减少不必要遍历次数的剪枝和为了实现返回数组中没有重复答案的去重。
为什么需要剪枝(剪枝的意义)?
因为代码的底层思路是双指针,左指针在最左端,右指针在最右端,四数之和如果大于target则right--,反之则left++。我们发现由于双指针中的right是从最右端也就是最大元素开始遍历的,那么如果此时target的值对于数组中的元素来说过小,则right会毫无意义的多次--才能达成四数之和等于target的要求(甚至可能出现数组中只需要一个数就大于target的情况,也就是说遍历到头一无所获),白白浪费了时间,这时候如果可以在遍历之前提前进行一次判断,如果数组中的最小的四个数的和都大于target的值,那么就可以直接跳出整个循环,可以节省时间提高效率,所以剪枝是对代码的一个优化操作。
第一次剪枝
if((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target){
break;
}这次操作的想法是,如果我此时最小的四个数之和都要大于target,那么剩下的数也就没有继续遍历的必要了,可以直接跳出最外层循环,return最终数组了。
第二次剪枝发生在第二层循环中(即三数循环之中,双指针之外),目的仍然是对于target可能过小这一种情况的优化。
if((long)nums[i]+nums[j]+nums[j+1]+nums[j+2]>target){
break;
}注意
第二次剪枝中如果判断target的值过小,则会break,跳出本循环,但是和第一次剪枝操作的不同之处在于:第一次剪枝操作会直接跳出最外层循环,return结束程序了,但第二次剪枝操作则是跳出内循环,继续外循环中下一个元素的循环,不会结束自身的循环(继续外循环中i=i+1时的内循环)也不会结束程序。
n=nums.length-1;
if((long)nums[n]+nums[n-1]+nums[n-2]+nums[n-3]<target){
return quadruplets;//quadruplets为最终返回数组
}if(i>0&&nums[i]==nums[i-1]){
continue;
}注意
我们不可以写成nums[i]==nums[i+1]这样,因为nums[i+1]其实就是nums[j],如果这样子写的话,我们的目的就从去重变成了避免目标数组中出现相同元素的情况了,使得最终答案收集不全,比如这种情况: nums[]={1,1,5,20,30},target=27。很明显这种情况下 1 1 5 20是满足条件的一组答案,但是由于第一个元素和第二个元素相等了,就被nums[i]==nums[i+1]这种判断给捕获筛去(continue)了, 所以要写成nums[i]==nums[i-1]这样来确保nums[j]和nums[i]相等时依然可以继续执行下去。(记得确保i>0,否则会报数组越界异常)
第二类去重:目的是为了减少不必要的循环次数,是一种优化
if((long)nums[i]+nums[length-3]+nums[length-2]+nums[length-1]<target){
continue;
}这种去重的思路是:判断在此时i的索引情况下的最大四数之和是否还要小于target的值。
如果小于,那么后面就不用继续执行(continue)了,继续下一次循环,试一试i=i+1时是否满足条件。
去重的总结(1):通过第一类去重的使用,去掉了由于数组中存在相同元素导致的多余循环次数(还能使得答案数组中可以有相同的元素)
比如nums[]={1,1,1,2,2,2,3,3,3,4,4,4,5,6}.这种情况下如果不去重那么肯定会有多次一模一样的循环。
第一次去重(最外层循环中的去重)中nums[i]==nums[i-1]{continue},确保了nums[i]和nums[j]元素的值相等的时候依然可以正常运行下去不会被continue。
第二次去重(内层循环的去重)中nums[j]==nums[j-1]{contiue},确保了第二个元素和第三个元素的值相等的时候依然可以正常运行下去。
由于nums[left]与nums[right]的值是否相等都不会被continue,所以我们可以说通过第一类去重分别在两个循环中的两次使用来,答案数组中的四个元素的值全部相等的情况依然可以被我们囊括其中。
去重的总结(2):通过第二类去重的使用,减少了在某个特定循环次数下无意义的循环次数(在i=n时,target过大导致的无意义循环)
通过第二类去重分别在两个循环中的使用,使得无意义遍历可以快速结束,节省时间,是一种优化。
总结
四数之和是对三数之和的拔高,代码实现上它在三数之和上又加了层循环。它不仅考察了去重还考察了剪枝,其中去重有两类,在两层循环中每一类均有使用,一共使用了四次。剪枝有一类(按题解),在两层循环中均有使用,使用了两次。
优化:针对如果target过大这种情况的剪枝(避免在第一次循环的第二个去重操作中的多余循环,节省时间):
操作:在进入循环之前,代码之初就进行一次判断,判断是否target对于数组来说过大,如果是,那么就直接return
if(((long)nums[n.length-1]+nums[n.length-2]+...nums[n.length-4])<target){
return quadruplets;
}class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>>quadruplets=new ArrayList<List<Integer>>();
if(nums==null||nums.length<4){
return quadruplets;
}
Arrays.sort(nums);
int length=nums.length;
if(((long)nums[length-1]+nums[length-2]+nums[length-3]+nums[length-4])<target){
return quadruplets;
}
for(int i=0;i<length-3;i++){
if(i>0&&nums[i]==nums[i-1]){
continue;
}
if((long)nums[i]+nums[i+1]+nums[i+2]+nums[i+3]>target){
break;
}
if((long)nums[i]+nums[length-3]+nums[length-2]+nums[length-1]<target){
continue;
}
for(int j=i+1;j<length-2;j++){
if(j>i+1&&nums[j]==nums[j-1]){
continue;
}
if((long)nums[i]+nums[j]+nums[j+1]+nums[j+2]>target){
break;
}
if((long)nums[i]+nums[j]+nums[length-2]+nums[length-1]<target){
continue;
}
int left=j+1,right=length-1;
while(left<right){
long sum=(long)nums[i]+nums[j]+nums[left]+nums[right];
if(sum==target){
quadruplets.add(Arrays.asList(nums[i],nums[j],nums[left],nums[right]));
while(left<right&&nums[left]==nums[left+1]){
left++;
}
left++;
while(left<right&&nums[right]==nums[right-1]){
right--;
}
right--;
}else if(sum<target){
left++;
}else{
right--;
}
}
}
}
return quadruplets;
}
}