nSum问题解题套路4.5-4.6

4.5 2sum问题的核心思想

4.5.1 2SUM-1

  • 给你输入一个数组numstarget可以保证在数组存在两个数的和为target,请你返回这两个数的索引

直接的想法是穷举任意两个数的组合,然后去试验符不符合要求,如果想要让时间复杂度下降,一般的方法就是用空间换时间,可以通过一个哈希表记录元素值到索引的映射,减少时间复杂度

    private Map<Integer,Integer> map = new HashMap<>();
    public int[] twoSum(int[] nums, int target) {
        for(int i = 0;i<nums.length;i++){
            map.put(nums[i],i);
        }
        for(int i = 0;i<nums.length;i++){
            int other = target - nums[i];
            //如果other存在而且不是nums[i]本身,那么就返回这两个索引
            if(map.containsKey(other) && map.get(other) != i){
                return new int[]{i,map.get(other)};
            }
        }
        return new int[]{-1,-1};
    }
  • 复杂度分析:这样,由于哈希表的查询时间为O(1),算法的时间复杂度降低到O(N),但是需要O(N)的空间复杂度来存储哈希表。

4.5.2 2SUM-2

这里稍微修改一下前面的问题,设计一个类,拥有两个API:

    private Map<Integer,Integer> freq = new HashMap<>();
    public void add(int number){
        //记录number出现的次数
        freq.put(number,freq.getOrDefault(number,0)+1);
    }
    public boolean find(int value){
        for (Integer key : freq.keySet()) {
            int other = value - key;
            //情况一
            if(other == key && freq.get(key) >1){
                return true;
            }
            //情况二
            if(other!=key && freq.containsKey(other) ){
                return true;
            }
        }
        return false;
    }
  • 涉及到find的时候有两种情况
    • 情况一:add了[3,3,2,5]之后,执行find(6),由于3出现了两次,3+3=3,所以返回true
    • 情况二:add了[3,3,2,5]之后,执行find(7),那么当key为2,other为5的时候,算法返回true
    • 复杂度分析:add的操作的时间复杂度为O(1),find方法的时间复杂度为O(N),空间复杂度均为O(N)
  • 对于频繁使用find方法的场景,可以尝试借助哈希集合来有针对性地优化find方法
    Set<Integer> sum = new HashSet<>();
    List<Integer> nums = new ArrayList<>();
    public void add(int number){
        //记录所有可能组成的和
        for (Integer n : nums) {
            sum.add(n+number);
        }
        nums.add(number);
    }
    public boolean find(int value){
        return sum.contains(value);
    }

这样sum中存储了所有加入数字可能组成的和,每次find只需要花费O(1)的时间在集合中判断是否存在,但是代价也很明显,最坏情况下add后sum的大小都会翻一倍,所以空间复杂度是O(2^N)

4.5.3 最后总结

对于2sum问题,一个难点就是给的数组无序,对于一个无序的数组,似乎只能暴力穷举所有的可能

一般情况下,我们会首先把数组排序再考虑双指针到的技巧,2Sum启发我们,HashMap或者HashSet也可以帮助我们处理无序数组的相关问题,如果给定的数组是有序的,那么算法应该这样写

    public int[] twoSum(int[] nums, int target) {
        int left = 0,right = nums.length-1;
        while(left<right){
            int sum = nums[left] + nums[right];
            if(target == sum){
                return new int[]{left,right};
            }else if(sum <target){
                left++;
            }else if(sum > target){
                right++;
            }
        }
        return new int[]{-1,-1};
    }

4.6 一个函数解决nSum问题

4.6.1 问题泛化

  • 假设输入一个数组nums和一个目标target,请返回nums中能够凑出target的两个元素的值,比如输入nums=[1,3,5,6],target=9,那么算法返回两个元素[3,6],假设有且只有一对元素可凑出target

这个问题可以先对nums进行排序,然后双指针向中间逼近就可以了

  • 问题泛化:nums中可能有多对元素之和等于target,请返回所有和为target的元素对,其中不能出现重复

对于修改之和的问题,关键难点是选择可能有多个和为target的数对,还不能重复,比如说[1,3][3,1]就算重复,只能算一次

首先,对于这道题而言基本思路还是双指针+排序,先初步写出代码

    List<List<Integer>> twoSumTarget(int[] nums,int target){
        Arrays.sort(nums);//先对数组进行排序
        List<List<Integer>> ans = new ArrayList<>();
        int left = 0,right = nums.length-1;
        while(left<right){
            int sum = nums[left] + nums[right];
            //根据sum和target的比较,移动左右指针
            if(sum<target){
                left++;
            }
            if(sum>target){
                right++;
            }
            if(sum == target){
                List<Integer> t = new ArrayList<>();
                t.add(left);t.add(right);
                ans.add(t);
                left++;right--;
            }
        }
        return ans;
    }

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

问题处在sum == target的if条件分支,当给res加入一次结果后,left和right在改变1的同时,还应该跳过所有重复的元素,也就是说看它移动的下一个位置的元素,是不是和当前的值一样,当然了,有的可能会有这种想法,新建一个set来保证元素不重复不就可以了吗,当然是可以,但是这样做依然无法避免大量的重复不必要计算。

            if(sum == target){
                List<Integer> t = new ArrayList<>();
                t.add(left);t.add(right);
                ans.add(t);
                //跳过所有的重复元素
                while(left<right && nums[left] == leftVal){
                    left++;
                }
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }

这样就可以保证一个答案只被添加一次,重复的结果都会被跳过,可以得到正确答案,那么前面的两个if分支也可以做一下这种优化,跳过相同的元素。完整代码如下:

    List<List<Integer>> twoSumTarget(int[] nums,int target){
        Arrays.sort(nums);//先对数组进行排序
        List<List<Integer>> ans = new ArrayList<>();
        int left = 0,right = nums.length-1;
        while(left<right){
            int sum = nums[left] + nums[right];
            int leftVal = nums[left];
            int rightVal = nums[right];
            //根据sum和target的比较,移动左右指针
            if(sum<target){
                while(left<right && nums[left] == leftVal){
                    left++;
                }
            }
            if(sum>target){
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }
            if(sum == target){
                List<Integer> t = new ArrayList<>();
                t.add(left);t.add(right);
                ans.add(t);
                //跳过所有的重复元素
                while(left<right && nums[left] == leftVal){
                    left++;
                }
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }
        }
        return ans;
    }
  • 复杂度分析:双指针操作的时间复杂度为O(N),排序的时间复杂度为O(NlogN),时间复杂度为O(NlogN)

4.6.2 解决3Sum问题

  • 给你输入一个数组nums,请你判断其中是否存在三个元素a,b,c使得a+b+c=0,如果有的话请找出所有满足条件而且不重复的三元组

这个问题的解决思路可以这样思考,首先三个数,第一个数字我们可以通过穷举nums[i]的任意一个元素来得到,后面两个元素转换为target-nums[i],然后再把这个target-nums[i]作为新的target丢到2Sum问题去解决就可以了

    public List<List<Integer>> threeSum(int[] nums) {
        return threeSumTarget(nums,0);
    }

    public List<List<Integer>> threeSumTarget(int[] nums,int target){
        //输入数组nums,返回所有和为target的三元组
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        //穷举threeSum的第一个数
        for(int i = 0;i<n;i++){
            //对target-nums[i]计算twoSum
            List<List<Integer>> tuples = twoSumTarget(nums, target - nums[i], i + 1);
            //如果存在满足条件的二元组,再加上nums[i]就是符合条件的三元组
            for (List<Integer> tuple : tuples) {
                tuple.add(nums[i]);
                ans.add(new ArrayList<>(tuple));
            }
            //跳过第一个数字重复的情况,否则会出现重复的结果
            while(i<n-1 && nums[i] == nums[i+1]){
                i++;
            }
        }
        return ans;
    }

    /**
     *
     * @param nums
     * @param target
     * @param start
     * @return
     */
    List<List<Integer>> twoSumTarget(int[] nums,int target,int start){//左指针改为从start开始,其他不变
        List<List<Integer>> ans = new ArrayList<>();
        int left = start,right = nums.length-1;
        while(left<right){
            int sum = nums[left] + nums[right];
            int leftVal = nums[left];
            int rightVal = nums[right];
            //根据sum和target的比较,移动左右指针
            if(sum<target){
                while(left<right && nums[left] == leftVal){
                    left++;
                }
            }
            if(sum>target){
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }
            if(sum == target){
                List<Integer> t = new ArrayList<>();
                t.add(leftVal);t.add(rightVal);
                ans.add(t);
                //跳过所有的重复元素
                while(left<right && nums[left] == leftVal){
                    left++;
                }
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }
        }
        return ans;
    }
  • 复杂度分析:排序的时间复杂度为O(NlogN),双指针操作为O(N),函数在for循环中调用twoSumTarget,所以就是O(NlogN+N2)=O(N2)

4.6.3 解决4Sum问题

  • 4Sum的解决思路也是一致的,只需要加上一个穷举nums[i]的过程即可
    public List<List<Integer>> fourSum(int[] nums, int target) {
        return fourSumTarget(nums,target);
    }

    public List<List<Integer>> fourSumTarget(int[] nums,long target){
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<>();
        for(int i = 0;i<nums.length;i++){
            List<List<Integer>> tuples = threeSumTarget(nums,target-(long)nums[i],i+1);
            for (List<Integer> tuple :tuples){
                tuple.add(nums[i]);
                ans.add(new ArrayList<>(tuple));
            }
            while(i<nums.length-1 && nums[i] == nums[i+1]){
                i++;
            }
        }
        return ans;
    }


    public List<List<Integer>> threeSumTarget(int[] nums,long target,int start){
        //输入数组nums,返回所有和为target的三元组
        int n = nums.length;
        List<List<Integer>> ans = new ArrayList<>();
        //穷举threeSum的第一个数
        for(int i = start;i<n;i++){
            //对target-nums[i]计算twoSum
            List<List<Integer>> tuples = twoSumTarget(nums, (target - (long)nums[i]), i + 1);
            //如果存在满足条件的二元组,再加上nums[i]就是符合条件的三元组
            for (List<Integer> tuple : tuples) {
                tuple.add(nums[i]);
                ans.add(new ArrayList<>(tuple));
            }
            //跳过第一个数字重复的情况,否则会出现重复的结果
            while(i<n-1 && nums[i] == nums[i+1]){
                i++;
            }
        }
        return ans;
    }

    /**
     *
     * @param nums
     * @param target
     * @param start
     * @return
     */
    List<List<Integer>> twoSumTarget(int[] nums,long target,int start){//左指针改为从start开始,其他不变
        List<List<Integer>> ans = new ArrayList<>();
        int left = start,right = nums.length-1;
        while(left<right){
            long sum = nums[left] + nums[right];
            long leftVal = nums[left];
            long rightVal = nums[right];
            //根据sum和target的比较,移动左右指针
            if(sum<target){
                while(left<right && nums[left] == leftVal){
                    left++;
                }
            }
            if(sum>target){
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }
            if(sum == target){
                List<Integer> t = new ArrayList<>();
                t.add((int)leftVal);t.add((int)rightVal);
                ans.add(t);
                //跳过所有的重复元素
                while(left<right && nums[left] == leftVal){
                    left++;
                }
                while (left<right && nums[right] == rightVal){
                    right--;
                }
            }
        }
        return ans;
    }
  • 注意一组样例:[1000000000,1000000000,1000000000,1000000000] -294967296这组样例是新加的,如果只是用int的话就会溢出,在涉及到加减的地方该为long的数据类型即可解决该问题

4.6.4 解决nSum问题

  • 让我们回想一下刚才的解题过程,有没有发现其实除了2Sum以外,更高次数的sum都是基于2Sum来做枚举的?而且问题的解决办法都是一样的?
  • 所以可以得出结论:base-case:2Sum,通过不断转移数组下标来转移状态,
    //注意:调用这个函数之前,nums必须是排好序的
    List<List<Integer>> nSumTarget(int[] nums,int n,int start,long target){
        int size = nums.length;
        List<List<Integer>> ans =  new ArrayList<>();
        //至少是2Sum而且,而且数组大小不能小于n
        if(n<2 || size<n) {
            return ans;
        }
        //base-case
        if(n==2){
            int lo = start ,hi = size-1;
            while(lo < hi){
                long sum = (long)nums[lo] + (long)nums[hi];
                int left = nums[lo];int right = nums[hi];
                if(sum< target){
                    while (lo<hi && nums[lo] == left){
                        lo++;
                    }
                }else if(sum > target){
                    while (lo<hi && nums[hi] == right){
                        hi--;
                    }
                }else {
                    List<Integer> t = new ArrayList<>();
                    t.add(left);t.add(right);
                    while (lo<hi && nums[lo] == left){
                        lo++;
                    }
                    while (lo<hi && nums[hi] == right){
                        hi--;
                    }
                }
            }
        }else{
            //n>2的时候直接递归计算即可
            for(int i = start;i<size;i++){
                List<List<Integer>> tuples = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
                for (List<Integer> tuple : tuples) {
                    tuple.add(nums[i]);
                    ans.add(new ArrayList<>(tuple));
                }
                while (i<size-1 && nums[i] == nums[i+1]){
                    i++;
                }
            }
        }
       return ans;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值