LeetCode-解决nSum问题

nSum问题

两数之和

题目描述:

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

利用双指针解决,代码如下:

public class TwoSum {
    public int[] twoSum(int[] nums, int target) {
        // 因为后边要对数组排序,要返回原数组的下标,所以这里拷贝一份
        int[] numsCopy = (int[]) Arrays.copyOf(nums,nums.length);
        Arrays.sort(nums);
        int lo = 0,hi = nums.length-1;
        int[] res = new int[2];
        while(lo < hi) {
            int sum = nums[lo] + nums[hi];
            if(sum<target) {
                lo++;
            }else if(sum>target) {
                hi--;
            }else if(sum == target){
                break;
            }
        }
        for(int i=0;i<numsCopy.length;i++) {
            // 这里要防止第一个下标和第二个下标重复
            if(lo<numsCopy.length&&numsCopy[i] == nums[lo]) {
                res[0] = i;
                // 赋值之后就不会再进入这个if体内,保证了lo不会后移
                lo = numsCopy.length;
            }else if(hi<numsCopy.length&&numsCopy[i] == nums[hi]) {
                res[1] = i;
                hi = numsCopy.length;
            }else if(lo == hi) {
                return res;
            }
        }
        return res;
    }
}

继续魔改题目,把这个题目变得更泛化,更困难一点:

nums 中可能有多对儿元素之和都等于 target,请你的算法返回所有和为 target 的元素对儿,其中不能出现重复

比如说输入为 nums = [1,3,1,2,2,3], target = 4,那么算法返回的结果就是:[[1,3],[2,2]]

对于修改后的问题,关键难点是现在可能有多个和为 target 的数对儿,还不能重复,比如上述例子中 [1,3][3,1] 就算重复,只能算一次。

首先,基本思路肯定还是排序加双指针:

但是,这样实现会造成重复的结果,比如说 nums = [1,1,1,2,2,3,3], target = 4,得到的结果中 [1,3] 肯定会重复。

出问题的地方在于 sum == target 条件的 if 分支,当给 res 加入一次结果后,lohi 不应该改变 1 的同时,还应该跳过所有重复的元素:

image

所以,可以对双指针的 while 循环做出如下修改:

while(lo < hi) {
    int left = nums[lo];
    int right = nums[hi];
    int sum = left +right;
    if(sum<target) {
        // 跳过所有重复的元素
        while(lo<hi&&nums[lo] == left){
            lo++;
        }
    }else if(sum > target) {
        // 跳过所有重复的元素
        while(lo<hi&&nums[hi] == right) {
            hi--;
        }
    }else if(sum == target) {
        res.add(Arrays.asList(nums[lo],nums[hi]));
        // 跳过所有重复的元素
        while(lo<hi&&nums[lo] == left){
            lo++;
        }
        while(lo<hi&&nums[hi] == right) {
            hi--;
        }
    }
}

一个通用化的 twoSum 函数就写出来了.

3Sum问题

题目描述:

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

再泛化一下题目,不要光和为 0 的三元组了,计算和为 target 的三元组吧,同上面的 twoSum 一样,也不允许重复的结果:

利用穷举的思想,想找和为 target 的三个数字,那么对于第一个数字,可能是什么?nums 中的每一个元素 nums[i] 都有可能!

那么,确定了第一个数字之后,剩下的两个数字可以是什么呢?其实就是和为 target - nums[i] 的两个数字呗,那不就是 twoSum 函数解决的问题么🤔

可以直接写代码了,需要把 twoSum 函数稍作修改即可复用:

public class ThreeSum {

    public List<List<Integer>> threeSum(int[] nums,int target) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<>();

        for(int i=0;i<n;i++) {
            List<List<Integer>> temps = getTwoSum(nums,i+1,target-nums[i]);
            for (List<Integer> temp : temps) {
                // temp是java.util.Arrays.ArrayList,他是Arrays类下的一个内部类,并没有实现add方法和remove方法,需要转化为java.util.ArrayList
                List<Integer> list=new ArrayList(temp);
                list.add(nums[i]);
                res.add(list);
            }
            // 跳过第一个数字重复的情况,否则会出现重复结果
            while(i<n-1&&nums[i] == nums[i+1]) {
                i++;
            }
        }
        return res;
    }

    private List<List<Integer>> getTwoSum(int[] nums,int start,int target) {

        List<List<Integer>> res = new ArrayList<>();
        int lo = start ,hi = nums.length-1;
        while(lo < hi) {
            int left = nums[lo];
            int right = nums[hi];
            int sum = left +right;
            if(sum<target) {
                // 跳过所有重复的元素
                while(lo<hi&&nums[lo] == left){
                    lo++;
                }
            }else if(sum > target) {
                // 跳过所有重复的元素
                while(lo<hi&&nums[hi] == right) {
                    hi--;
                }
            }else if(sum == target) {
                res.add(Arrays.asList(nums[lo],nums[hi]));
                // 跳过所有重复的元素
                while(lo<hi&&nums[lo] == left){
                    lo++;
                }
                while(lo<hi&&nums[hi] == right) {
                    hi--;
                }
            }
        }
        return res;
    }
}

关键点在于,不能让第一个数重复,至于后面的两个数,我们复用的 twoSum 函数会保证它们不重复。所以代码中必须用一个 while 循环来保证 3Sum 中第一个元素不重复。

至此,3Sum 问题就解决了,时间复杂度不难算,排序的复杂度为 O(NlogN)twoSumTarget 函数中的双指针操作为 O(N)threeSumTarget 函数在 for 循环中调用 twoSumTarget 所以总的时间复杂度就是 O(NlogN + N^2) = O(N^2)

4sum问题

题目描述:

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意:答案中不可以包含重复的四元组。

4Sum 完全就可以用相同的思路:穷举第一个数字,然后调用 3Sum 函数计算剩下三个数,最后组合出和为 target 的四元组。

public class FourSum {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        for(int i = 0;i<n;i++) {
            List<List<Integer>> threes = threeSum(nums,i+1,target-nums[i]);
            for (List<Integer> three : threes) {
                // temp是java.util.Arrays.ArrayList,他是Arrays类下的一个内部类,并没有实现add方法和remove方法,需要转化为java.util.ArrayList
                List<Integer> list=new ArrayList(three);
                list.add(nums[i]);
                res.add(list);
            }
            // 跳过第一个数字重复的情况,否则会出现重复结果
            while(i<n-1&&nums[i] == nums[i+1]) {
                i++;
            }
        }
        return res;
    }
    private List<List<Integer>> threeSum(int[] nums,int start,int target) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<>();

        for(int i=start;i<n;i++) {
            List<List<Integer>> temps = getTwoSum(nums,i+1,target-nums[i]);
            for (List<Integer> temp : temps) {
                // temp是java.util.Arrays.ArrayList,他是Arrays类下的一个内部类,并没有实现add方法和remove方法,需要转化为java.util.ArrayList
                List<Integer> list=new ArrayList(temp);
                list.add(nums[i]);
                res.add(list);
            }
            // 跳过第一个数字重复的情况,否则会出现重复结果
            while(i<n-1&&nums[i] == nums[i+1]) {
                i++;
            }
        }
        return res;
    }

    private List<List<Integer>> getTwoSum(int[] nums,int start,int target) {

        List<List<Integer>> res = new ArrayList<>();
        int lo = start ,hi = nums.length-1;
        while(lo < hi) {
            int left = nums[lo];
            int right = nums[hi];
            int sum = left +right;
            if(sum<target) {
                while(lo<hi&&nums[lo] == left){
                    lo++;
                }
            }else if(sum > target) {
                while(lo<hi&&nums[hi] == right) {
                    hi--;
                }
            }else if(sum == target) {
                res.add(Arrays.asList(nums[lo],nums[hi]));
                while(lo<hi&&nums[lo] == left){
                    lo++;
                }
                while(lo<hi&&nums[hi] == right) {
                    hi--;
                }
            }
        }
        return res;
    }
}

按照相同的套路,4Sum 问题就解决了,时间复杂度的分析和之前类似,for 循环中调用了 threeSumTarget 函数,所以总的时间复杂度就是 O(N^3)

nSum问题:

先附上代码:

public class NSum {
    // 思路:把之前的题目解法合并起来了,n == 2 时是 twoSum 的双指针解法,n > 2 时就是穷举第一个数字,然后递归调用计算 (n-1)Sum,组装答案
    // 这里的参数n是指nSum里的n
    public List<List<Integer>> nSum(int[] nums, int n,int start,int target) {
        // Arrays.sort(nums);
        int len = nums.length;
        List<List<Integer> > res = new ArrayList<>();
        if(n<2||len<n) {
            return res;
        }
        if(n==2) {
            // 双指针那一套
            int lo = start ,hi = len-1;
            while(lo < hi) {
                int left = nums[lo];
                int right = nums[hi];
                int sum = left +right;
                if(sum<target) {
                    while(lo<hi&&nums[lo] == left){
                        lo++;
                    }
                }else if(sum > target) {
                    while(lo<hi&&nums[hi] == right) {
                        hi--;
                    }
                }else if(sum == target) {
                    res.add(Arrays.asList(nums[lo],nums[hi]));
                    while(lo<hi&&nums[lo] == left){
                        lo++;
                    }
                    while(lo<hi&&nums[hi] == right) {
                        hi--;
                    }
                }
            }
        }else {
            // n > 2 时,递归计算 (n-1)Sum 的结果
            for(int i = start;i<len;i++) {
                List<List<Integer>> temps = nSum(nums,n-1,i+1,target-nums[i]);
                for(List<Integer> temp:temps) {
                    List<Integer> list = new ArrayList<>(temp);
                    list.add(nums[i]);
                    res.add(list);
                }
                while(i<n-1&&nums[i] == nums[i+1]) {
                    i++;
                }
            }
        }
        return res;
    }
}

实际上就是把之前的题目解法合并起来了,n == 2 时是 twoSum 的双指针解法,n > 2 时就是穷举第一个数字,然后递归调用计算 (n-1)Sum,组装答案。

需要注意的是,调用这个 nSum 函数之前一定要先给 nums 数组排序

posted @ 2021-04-26 21:17  RealGang  阅读(117)  评论(0编辑  收藏  举报