两数之和(two sum)问题通用解法(map法和双指针)

岁酱吖の 提交于 2019-12-27 20:33:27

这类题目都是套路非常明显的题目,掌握下述的题目后应该对这类问题没有任何问题。

1. LeetCode01:two sum

题意

  • 给定数组和一个目标数,要求在数组中找到两个数,其和等于目标数,返回该两数的下标

思路1:map法

  • 建立从value->index的映射
  • 遍历数组
    • 依次检验当前mapkey中是否包含target - num[i]
      • 如果包含,那么说明一定存在nums[j] = target - nums[i]已经在map中,且一定j<i,那么只需要通过targte - nums[i]map中找到下标j,那么j和i就是结果
      • 如果不包含,则把当前的nums[i]和i添加到map中即可

代码1:

 public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        if (nums == null || nums.length == 0){
            return null;
        }
        //0. num->index
        Map<Integer,Integer> map = new HashMap<>();

        for (int i = 0; i < nums.length; i++) {

            if (map.containsKey(target - nums[i])){
                res[0] = map.get(target - nums[i]);
                res[1] = i;
                return res;
            }
            map.put(nums[i],i);
        }
        return res;
    }

思路2:双指针解法

  • 在有序数组中可以利用两个相向双指针,一个从左往右,一个从右往左的遍历数组,然后求出题目要求(两数之和等于/大于/小于/最接近于某个数)的答案即可
    • 所以双指针解法的第一步永远是排序
  • 如果题目要求返回的值是索引,此时如果直接对数组进行排序,那么就会丢失掉题目给定数组中数组元素和索引的对应关系,此时通用的解决办法是利用一个Pair来存储value和index的对应关系,然后对Pairvalue进行排序
  • 当得到有序数组后,双指针的逻辑就很简单了(见代码中的注释部分)

代码2

class Solution {
    class Pair{
        int val;
        int index;
         public Pair(int val, int index){
             this.val = val;
             this.index = index;
         }
    }

    public int[] twoSum(int[] nums, int target) {
        if (nums == null || nums.length == 0){
            return null;
        }
        int[] res = new int[2];

        Pair[] pairs = new Pair[nums.length];
        //0. 将数组元素值和index进行绑定
        for (int i = 0; i < nums.length; i++) {
            pairs[i] = new Pair(nums[i],i);
        }
        //1. 双指针解法的第一步永远是排序
        Arrays.sort(pairs,new Comparator<Pair>(){
            @Override
            public int compare(Pair o1, Pair o2) {
                return o1.val - o2.val;
            }
        });
        int left = 0;
        int right = nums.length - 1;

        while (left < right){
            if (pairs[left].val + pairs[right].val == target){
                res[0] = pairs[left].index;
                res[1] = pairs[right].index;
                return res;
            }else if (pairs[left].val + pairs[right].val > target){
                //2. 当前的最大值加上最小值 都还是大于目标值,说明最大值太大了
                right--;
            }else {
                //3.当前的最小值加上最大值 都还是小于目标值,说明最小值太小了
                left++;
            }
        }
        return null;
    }
}

小结

  • 上述两种思路是解决两数之和这类问题的通用解决手段

2. LintCode607. Two Sum III - Data structure design

题意

  • 实现一个数据结构,完成如下两个功能
  • void add(num):可以增加给定的数据
  • boolean find(value):查找当前数据集中是否存在两数之和等于value

思路

  • 首先,该数据结构需要存储数据,在设计数据结构的时候,底层的数据存储要么用链表要么用动态数组
  • 此题中并没有说明要存储数据的具体长度,那么就只能用动态数组,在Java中最常用的就是list
  • 其次,第二个功能的实现,就是一个两数之和问题,而且此题不要求返回两数的索引,只需要判断是否存在,那么显然考虑用map法,因为完全不需要考虑索引,完全可以把map简化为用一个set来做判断

代码

class Solution {

    //0. 用list来存数据
    List<Integer> list = new ArrayList<>();

    public void add(int number) {
        // write your code here
        list.add(number);
    }


    public boolean find(int value) {
        // write your code here
        Set<Integer> set = new HashSet<>();

        for (Integer num : list) {

            if (set.contains(value - num)){
                return true;
            }
            set.add(num);
        }
        return false;
    }
}

3. 167. Two Sum II - Input array is sorted

题意

给定有序数组,和一目标数,在数组中找到两数之和等于目标数,返回两数在数组中的次序(注意不是基0的索引,所以最后需要+1)

思路

  • 是第1题的简化版
  • 因为已经有序了,不需要再排序,自然也不需要处理排序后数组值和索引无法对应的问题
  • 直接用双指针来处理即可

代码

  public static int[] twoSum(int[] A, int target) {

        int[] res = new int[2];
        int left = 0;
        int right = A.length - 1;

        while (left < right){

            if (A[left] + A[right] == target){
                res[0] = left + 1;
                res[1] = right + 1;
                return res;
            }else if (A[left] + A[right] > target){
                //1. 和大了 要减小
                right--;
            }else {
                //2. 和小了 要增大
                left++;
            }
        }
        return null;

    }

4. LintCode587.Two Sum - Unique pairs

此题是vip,无法提供链接

题意

  • 给定一个数组和目标数,找到数组中有多少组不同的元素对,其和都等于给定的目标数,返回不同的元素对的对数
  • 例如:
    • nums=[1,1,2,45,46,46],target = 47
    • 返回2
    • 因为:1 + 46 = 47、2 + 45 = 47

思路

  • 仍然是两数之和问题,且不用返回下标,所以无需考虑数值和下标的问题
  • 关键在于去重
  • 双指针和去重的一个首要操作都是排序
  • 去重的一般手段就是利用while来找到不再重复的元素(具体见代码和注释)

代码

 public int twoSum6(int[] nums, int target) {

        if (nums == null || nums.length == 0){
            return 0;
        }
        //0. 首先排序,因为此题不需要返回索引,所以无需考虑数值与下标的关系
        //0.1 此处还有一个重要的目的在于为后续去重做准备,(数组中去重问题一般都需要先排序)
        Arrays.sort(nums);
        int left = 0;
        int right = nums.length - 1;
        int cnt = 0;//计数

        while (left  < right){

            int v = nums[left] + nums[right];
            if (v == target){
                //1. 如果找到一对数,那么肯定计数+1,指针移动
                cnt++;
                left++;
                right--;
                //2. 去重,为了避免一对同样的数的和再组成target
                while (left < right && nums[right] == nums[right+1]){
                    right--;
                }
                while (left < right && nums[left] == nums[left-1]){
                    left++;
                }
            }else if (v > target){
                //3. 大了 要减小
                right--;
            }else {
                //小了 要增大
                left++;
            }
        }
        return cnt;
        
    }

5. LeetCode15. 3Sum

题意

  • 给定数组,要求找到满足a +b + c = 0的三元组,且无重复

思路

  • a + b + c = 0-> a +b = -c

  • 遍历数组,将每一个元素当作-c = target

  • 然后在数组剩余元素中寻找无重复的两数之和等于target的组合

  • 那么这时候问题就退化成了上面第4题

  • 同时要注意对target也要去重

    • 去重的一个通用的思路是用set
    • 当然数组中可以有更简单的方式
    if (i > 0 && nums[i] == nums[i-1]){
                continue;
            }
    

代码

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {

        List<List<Integer>> results = new ArrayList<>();
        List<Integer> item = new ArrayList<>();
        if (nums == null || nums.length < 3){
            return results;
        }

        //0. 先排序,无需返回index,所以直接排序即可
        Arrays.sort(nums);
        //1.遍历nums,把每一个元素当作target,然后在数组剩下的部分寻找无重复的两数之和等于target
        //  问题退化成了two sum
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            //2.1 避免重复的target

            if (i > 0 && nums[i] == nums[i-1]){
                continue;
            }
            //2.2 a + b + c = 0 -> a + b = -c
            int target = -nums[i];
            //2.3 保证数组中剩下的元素至少有两个
            if (n - i - 1 >= 2) {
                twoSum(i+1,target,nums,results);

            }
        }
        return results;
    }

    private void twoSum(int startIndex,  int target, int[] nums, List<List<Integer>> results) {

        int left = startIndex;
        int right = nums.length - 1;
        while (left < right){
            if (nums[left] + nums[right] == target){
                List<Integer> item = new ArrayList<>();
                item.add(-target);
                item.add(nums[left]);
                item.add(nums[right]);
                results.add(item);
                //指针移动
                left++;
                right--;
                while (left < right && nums[left] == nums[left-1]){
                    left++;
                }
                while (left < right && nums[right] == nums[right +1]){
                    right--;
                }
            }else if (nums[left] + nums[right] < target){
                //小了 要增大
                left++;
            }else {
                //大了 要减小
                right--;
            }
        }
    }
}

6. LintCode-382. Triangle Count

题意

给定数组,要求找出能够构成三角形的三元组的数量

思路

  • 需要知道三条边能够构成一个三角形的充要条件

    • 两边之和大于第三边或两边之差小于第三边(任意一个都是充要的)
  • 那么这道题就只需要遍历数组元素,将每一个元素都当作target

  • 然后寻找两数之和大于target的数量,所以仍然是一个两数之和的问题

  • 既然是两数之和问题,所以仍然是先排序

  • 一个比较关键的地方就是在于计数的问题

    • 如果我们固定nums[i],然后在[i+1,n-1]这个区间内去找满足这样的一个不等式的数量,那么就会出现求一个CN2C_N^2的组合数问题
    • 所以为了规避这个问题,我们从前面看,也就是固定nums[i],然后在区间[0,i-1]这个区间内去找满足这个不等式的两数数量
    • 具体的实现细节查看代码中的注释

代码

 public int triangleCount(int[] S) {
        // write your code here
        if ( S == null || S.length == 0){
            return 0;
        }
        Arrays.sort(S);
        int count = 0;
        for (int i = 0; i < S.length; i++) {
            //0. 以S[i] = target 在 区间[0,i-1]中寻找两数之和大于 target
            int left = 0;
            int right = i - 1;
            while (left < right){

                if (S[left] + S[right] > S[i]){
                    //1. 因为有序,所以 left 到 right区间内的任意两数之和都大于 S[i]
                    //1.1 例如 3 4 5 6 7
                    //    S[left] = 3  S[right] = 6 S[i] = 7
                    //    这里本质上是把 S[right] 和S[i]都固定了,然后偶 left 到 right中的任一个数加上S[right]和S[i]
                    //    都满足条件,所以数量就是从left 到 right-1 直接的数量,也就是right - left
                    count += right - left;
                    //2. 现在改变right,让if中的左边的和小一点,看能否找到更多的解
                    right -=1;

                }else {
                    //3. 两数之和<=target,所以需要增大
                    left++;
                }
            }
        }
        return count;
    }

7. LintCode59. 3Sum Closest

题意

给定数组和目标数,要求在数组中找到一组三元组,其和离给定的目标数最小,返回这个和

思路

  • 三数之和的题目,一定都是先固定一个数,然后降问题退化为两数之和
  • 所以仍然是两数之和的题目,所以先排序
  • 这里是求全局最接近,凡是求最值型问题,一定需要维护一个或多个全局变量,与临时变量进行比较,然后更新
  • 所以,这道题目只需要依次遍历数组,固定nums[i],然后从区间[i+1,n-1]中寻找可能的离目标数最小的三个数
  • 相关细节在代码中

代码

  public int threeSumClosest(int[] numbers, int target) {

        if (numbers == null || numbers.length == 0){
            return 0;
        }
        Arrays.sort(numbers);
        //0. 定义全局变量(凡是求最值型问题,都一定需要定义这样的全局变量来和一些临时变量进行比较更新)
        int closestSum = Integer.MAX_VALUE;
        int closestDiff = Integer.MAX_VALUE;
        //1. 遍历数组,先固定每个元素, -2是因为从 i开始到最后至少需要三个元素 n -3 n-2 n-1
        for (int i = 0; i < numbers.length - 2; i++) {
            int e1 = numbers[i];
            int left = i+1;
            int right = numbers.length - 1;
            while (left < right){
                //2. 先求出当前的和以及和target的距离
                int tempSum = e1 + numbers[left] + numbers[right];
                int tempDiff = Math.abs(tempSum - target);
                //3. 如果当前距离更短则更新
                //3.1 这里有一个细节是,进入了if之后没有移动指针
                //     这样在下次的while循环中,一定会进入else分支,然后移动指针
                if (tempDiff < closestDiff){
                    closestDiff = tempDiff;
                    closestSum = tempSum;
                }else {
                    //4. 如果当前距离没有更短,则需要缩短tempSum和target的距离
                    //4.1 具体怎么缩短就需要看当前的具体情况了
                    if (tempSum < target){
                        left++;
                    }else {
                        right--;
                    }
                }
            }
        }
        return closestSum;
    }

8. LeetCode18-4Sum

题意

给定数组和一个目标数,找到4个数之和等于该目标数的组合,返回这些组合

思路

  • 遍历数组,依次固定两个数,然后把问题转换为两数之和
  • 注意对每一个数的去重

代码

 public List<List<Integer>> fourSum(int[] nums, int target) {


        List<List<Integer>> results = new ArrayList<>();
        if (nums == null || nums.length == 0){
            return results;
        }
        Arrays.sort(nums);
        //1. i后面至少还需要3个数 n-4 n-3 n-2 n-1
        for (int i = 0; i < nums.length - 3; i++) {
            //去重
            if (i > 0 && nums[i] == nums[i-1]){
                continue;
            }
            int e1 = nums[i];
            //1.1 j后面至少还要2个数 n-3 n-2 n-1
            for (int j = i+1; j < nums.length - 2; j++) {
                //去重 e1 和e2 是可以相等的
                if (j != i + 1 && nums[j] == nums[j-1]){
                    continue;
                }
                int e2 = nums[j];

                int left = j + 1;
                int right = nums.length - 1;
                while (left < right){

                    int sum = e1 + e2 + nums[left] + nums[right];
                    if (sum == target){
                        List<Integer> item = new ArrayList<>();
                        item.add(e1);
                        item.add(e2);
                        item.add(nums[left]);
                        item.add(nums[right]);
                        results.add(item);
                        left++;
                        right--;
                        //2. 去重
                        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 results;

    }

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!