【leetcode】两数之和、三数之和、四数之和解决方案

两数之和、三数之和、四数之和解决方案

这篇文章详细总结力扣网站中两数之和、三数之和、四数之和的解决方案

两数之和

两数之和题目🔗
题目描述
本题的要求是找到两个数 a a a, b b b,使其满足 a + b = t a r g e t a+b=target a+b=target
简单的解法是两重for循环分别找出 a a a b b b,这种方法效率比较低,这里我们使用哈希表解题。

(一)主要思想

哈希表的一个重要功能是快速判断一个元素是否出现集合里,根据这一特点,我们可以设置一个for循环遍历 a a a,然后判断 b b b 是否在哈希表里出现过。
如果 b b b 没有出现在哈希表里,则把 a a a 作为key,a对应的下标 i i i 为value加入到哈希表中;
如果 b b b 出现在哈希表里,则返回二者下标即可。

n u m s = [ 2 , 7 , 11 , 15 ] , t a r g e t = 9 nums = [2,7,11,15], target = 9 nums=[2,7,11,15],target=9 为例:
初始化一个HashMap,从下标为0遍历数组 n u m s nums nums:

i = 0 i=0 i=0 时, n u m s [ i ] = 2 nums[i]=2 nums[i]=2 ,此时 t a r g e t − a = 7 target-a=7 targeta=7,查询hashMap发现7不在表中,于是将(2,0)加入到hashMap中:

keyValue
20

i = 1 i=1 i=1 时, n u m s [ i ] = 7 nums[i]=7 nums[i]=7 ,此时 t a r g e t − a = 2 target-a=2 targeta=2,查询hashMap发现2在hashMap中,符合题意,返回2和7的下标0,1即可。

(二)代码实现

Java版:

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        Map<Integer, Integer> map= new HashMap();
        for(int i=0;i<nums.length;i++){
            int remain = target-nums[i];
            if(map.containsKey(remain)){
                res[0] = i;
                res[1] = map.get(remain);
            }else{
                map.put(nums[i],i);
            }
        }
        return res;
    }
}

C语言版(需要自己定义hashMap结构):

typedef struct {
     int key;
     int value;
     UT_hash_handle hh; // make this structure hashable
 } map;

map* hashMap = NULL;

 void hashMapAdd(int key, int value){
     map* s;
     // key already in the hash?
     HASH_FIND_INT(hashMap, &key, s);
     if(s == NULL){
         s = (map*)malloc(sizeof(map));
         s -> key = key;
         HASH_ADD_INT(hashMap, key, s);
     }
     s -> value = value;
 }

map* hashMapFind(int key){
     map* s;
     // *s: output pointer
     HASH_FIND_INT(hashMap, &key, s);   
     return s;
 }

 void hashMapCleanup(){
     map* cur, *tmp;
     HASH_ITER(hh, hashMap, cur, tmp){
         HASH_DEL(hashMap, cur);
         free(cur);
     }
 }

 void hashPrint(){
     map* s;
     for(s = hashMap; s != NULL; s=(map*)(s -> hh.next)){
         printf("key %d, value %d\n", s -> key, s -> value);
     }
 }


int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
    int *ans = malloc(sizeof(int) * 2);
    map* res = NULL;
    hashMap = NULL;
    for(int i=0;i<numsSize;i++){
        res = hashMapFind(target-nums[i]);
        if(res){
            ans[0] = res->value;
            ans[1] = i;
            *returnSize = 2;
            return ans;
        }else{
            hashMapAdd(nums[i],i);
        }
    }
    hashMapCleanup();
    return NULL;
}
(三)总结

在此题中,对数组进行遍历并使用hashMap记录遍历过的元素,在遍历的过程中不断查询hashMap,判断已经遍历过的元素中是否存在满足 a + b = t a r g e t a+b=target a+b=target 的元素。

三数之和

三数之和题目🔗
在这里插入图片描述
参照两数之和,这道题目也可以用两重for循环+hashMap来解决。 a + b + c = 0 a+b+c=0 a+b+c=0,使用一重for循环来确定a,一重for循环来确定b,然后通过 0 − a − b 0-a-b 0ab 来确定 c c c 即可。但是这种方式需要复杂地去重。

例如: n u m s = [ − 1 , − 1 , − 1 , 0 , 0 , 0 , 1 , 1 , 1 ] nums=[-1,-1,-1,0,0,0,1,1,1] nums=[1,1,1,0,0,0,1,1,1],其去重的复杂程度可想而知

so,这道题我们不妨换一种思路,通过排序+双指针的方式实现。

(一)主要思路

由于此题并未要求返回下标,我们先将数组按照升序排序。
然后设置一个for循环遍历排序后的数组,设置两个指针 l e f t left left r i g h t right right l e f t left left 指向 i + 1 i+1 i+1 的位置, r i g h t right right指向数组最后一个位置,如下图所示:
在这里插入图片描述
判断 s u m = n u m s [ i ] + n u m s [ l e f t ] + n u m s [ r i g h t ] sum=nums[i]+nums[left]+nums[right] sum=nums[i]+nums[left]+nums[right] 与0的大小关系:
s u m = 0 sum=0 sum=0,则将三个数加入到结果集中, l e f t + + , r i g h t − − left++,right-- left++,right
s u m > 0 sum>0 sum>0,则 r i g h t − − right-- right;
s u m < 0 sum<0 sum<0,则 l e f t − − left-- left;
直到 l e f t > r i g h t left>right left>right

n u m s = [ − 1 , 0 , 1 , 2 , − 1 , − 4 ] nums = [-1,0,1,2,-1,-4] nums=[1,0,1,2,1,4]为例:
首先对数组进行排序,排序后: n u m s = [ − 4 , − 1 , − 1 , 0 , 1 , 2 ] nums=[-4,-1,-1,0,1,2] nums=[4,1,1,0,1,2],指针的初始状态:
在这里插入图片描述
指针的移动过程如下图:
在这里插入图片描述
第二轮的指针移动过程如下:
在这里插入图片描述
第三轮指针移动过程如下:
在这里插入图片描述
后面几轮的移动过程类似,在此不再赘述。
根据距离可知,我们的方法行之有效,但也存在结果重复的问题,比如第二轮第三步得到的结果 ( − 1 , 0 , 1 ) (-1,0,1) (1,0,1)与第三轮第二步得到的结果重复。因此我们进行去重操作。

首先分析重复结果出现的原因:
在本例中,出现重复是因为 n u m s [ i ] nums[i] nums[i] n u m s [ i + 1 ] nums[i+1] nums[i+1]值相同,导致出现了两次 ( − 1 , 0 , 1 ) (-1,0,1) (1,0,1)的三元组。
因此我们在第二轮以后的每一轮的指针移动前,判断 n u m s [ i ] nums[i] nums[i] 是否等于 n u m s [ i − 1 ] nums[i-1] nums[i1] 即可。若二者相等,则跳过本轮循环。
代码如下:

 if(nums[i]>0) break; //如果i指向的数已经大于0了,那么后续的三元组均不满足题意
 if(i>0&&nums[i]==nums[i-1]) continue;

那么问题来了,这里为什么不用 n u m s [ i ] nums[i] nums[i] n u m s [ i + 1 ] nums[i+1] nums[i+1] 来做判断?
假设给出的数组 n u m s = [ − 1 , − 1 , 2 ] nums=[-1,-1,2] nums=[1,1,2],那么这个数值元素组成的三元组刚好是满足题意的,如果我们用 n u m s [ i ] = n u m s [ i + 1 ] nums[i]=nums[i+1] nums[i]=nums[i+1]来判断,在第一轮开始之前,判断出 n u m s [ 0 ] = n u m s [ 1 ] nums[0]=nums[1] nums[0]=nums[1],跳过本轮循环,那么结果集将会是空集,不满足题意。用 n u m s [ i ] nums[i] nums[i] n u m s [ i − 1 ] nums[i-1] nums[i1]则不会出现上述情况

除了 n u m s [ i ] nums[i] nums[i] 会重复, l e f t left left r i g h t right right 对应的值也会重复:
比如 n u m s = [ − 1 , 0 , 0 , 1 , 1 ] nums=[-1,0,0,1,1] nums=[1,0,0,1,1],则会出现两个 ( − 1 , 0 , 1 ) (-1,0,1) (1,0,1)的三元组,因此我们需要分别对 l e f t left left r i g h t right right 进行去重。
方法与对 n u m s [ i ] nums[i] nums[i] 的处理类似,每次保存结果后,我们只需让 l e f t left left 不断 + + ++ ++,直到 l e f t left left 与其前一个值不相等,然后让 r i g h t right right 不断 − − -- ,使其与后一个值不相等即可。
代码如下:

if(nums[i]+nums[left]+nums[right]==0){
                    List<Integer> sublist = new ArrayList<>();
                    sublist.add(nums[i]);
                    sublist.add(nums[left]);
                    sublist.add(nums[right]);
                    res.add(sublist);
                    left++;
                    right--;
                    while(left<right&&(nums[left]==nums[left-1])) left++; 
                    while(left<right&&(nums[right]==nums[right+1])) right--;
                }

这里考虑一个问题,为什么是在保存结果后再对 l e f t left left r i g h t right right 去重,而不是先去重再保存结果呢?
考虑数组 n u m s = [ 0 , 0 , 0 , 0 , 0 ] nums=[0,0,0,0,0] nums=[0,0,0,0,0],显然,三元组 ( 0 , 0 , 0 ) (0,0,0) (0,0,0)是符合题意的,如果是先去重,则会将此结果排除掉。

(二)代码实现
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>>res =new ArrayList<List<Integer>>();
        Arrays.sort(nums);
        int left,right;
        for(int i=0;i<nums.length;i++){
            if(nums[i]>0) break; //如果i指向的数已经大于0了,那么后续的三元组均不满足题意
            if(i>0&&nums[i]==nums[i-1]) continue;
            left = i+1;
            right = nums.length-1;
            while(left<right){
                if(nums[i]+nums[left]+nums[right]==0){
                    List<Integer> sublist = new ArrayList<>();
                    sublist.add(nums[i]);
                    sublist.add(nums[left]);
                    sublist.add(nums[right]);
                    res.add(sublist);
                    left++;
                    right--;
                    while(left<right&&(nums[left]==nums[left-1])) left++; 
                    while(left<right&&(nums[right]==nums[right+1])) right--;
                }
                else if(nums[i]+nums[left]+nums[right]>0){
                    right--;
                    
                }else{
                    left++;
                }
            }
        }
        return res;
    }
}
(三)总结

本题排序+双指针的思想比较好理解,关键在于三元组中每个数的去重处理,以及去重操作的时机。

四数之和

四数之和题目🔗
在这里插入图片描述
三书之和满足的条件是 a + b + c = 0 a+b+c=0 a+b+c=0,四数之和慢则的条件是 a + b + c + d = t a r g e t a+b+c+d=target a+b+c+d=target,万变不离其宗,我们照着葫芦画瓢即可。

(一)主要思想

首先对数组按照升序排序,设置两个for循环,第一个负责确定 a a a,第二个负责确定 b b b,剩下的 c c c d d d 依然由 l e f t left left r i g h t right right 指针确定。判断逻辑与三数之和相同。
n u m s = [ 1 , 0 , − 1 , 0 , − 2 , 2 ] , t a r g e t = 0 nums = [1,0,-1,0,-2,2], target = 0 nums=[1,0,1,0,2,2],target=0 为例,我们看一下指针的移动过程:
在这里插入图片描述
l e f t > r i g h t left>right left>right 跳出循环后, j + + j++ j++
其余指针移动过程与本轮类似,不再赘述。

然后我们考虑一下去重操作,与三数之和类似,我们需要分别对 a , b , c , d a,b,c,d a,b,c,d 四个数去重:

  • a a a 的去重操作与三数之和一样:
if(i>0&&nums[i]==nums[i-1]) continue;
  • b b b的去重稍有不同:
if((j-i)>1&&nums[j]==nums[j-1]) continue;

这里判断条件为什么是 j − i > 1 j-i>1 ji>1
考虑数组 n u m s = [ − 1 , − 1 , 2 , 2 ] , t a r g e t = 0 nums=[-1,-1,2,2],target=0 nums=[1,1,2,2]target=0即可,若直接判断 n u m s [ j ] 与 n u m s [ j − 1 ] nums[j]与nums[j-1] nums[j]nums[j1]的大小关系而不考虑 j j j的位置,满足条件的四元组 ( − 1 , − 1 , 2 , 2 ) (-1,-1,2,2) (1,1,2,2)将会直接被排除。

  • c c c d d d的去重与三数之和一样:
 if(sum==target){
    res.add(Arrays.asList(nums[i], nums[j],nums[left], nums[right]));
    left++;
    right--;
    while(left<right&&nums[left]==nums[left-1]) left++;
   while(left<right&&nums[right]==nums[right+1]) right--;
 }

这道题的测试用例中有int型数值相加溢出的情况,因此我们在计算时先将int转成long,代码如下:

long sum = (long)nums[i]+nums[j]+nums[left]+nums[right];
(二)代码实现
class Solution {
   public List<List<Integer>> fourSum(int[] nums, int target) {
       List<List<Integer>> res = new ArrayList<List<Integer>>();
       int left,right;
       Arrays.sort(nums);
       Boolean isbreak = true;
       for(int i=0;i<nums.length-3&&isbreak;i++){
            if (nums[i] > 0 && nums[i] > target) { //先做第一步的剪枝,注意与三数之和区别开来
              isbreak = false;
              break;
           }
           for(int j=i+1;j<nums.length-2;j++){
               left = j+1;
               right = nums.length-1;
               if(i>0&&nums[i]==nums[i-1]) continue;
               if((j-i)>1&&nums[j]==nums[j-1]) continue;
               while(left<right){
                   long sum = (long)nums[i]+nums[j]+nums[left]+nums[right];
                   if(sum==target){
                       res.add(Arrays.asList(nums[i], nums[j],nums[left], nums[right]));
                       left++;
                       right--;
                       while(left<right&&nums[left]==nums[left-1]) left++;
                       while(left<right&&nums[right]==nums[right+1]) right--;
                   }
                   else if(sum>target){
                       right--;
                   }else{
                       left++;
                   }
               }
           }
       }
       return res;
   }
}

总结

本篇文章记录了力扣三道典型题目的思路,除了要掌握双指针+排序的思想外,去重剪枝的处理也十分关键。在做题的过程中应多写多练,提高自己思维的严谨性~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

<Yesterday>

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值