一个函数秒杀2Sum 3Sum 4Sum问题

一个函数秒杀2sum 3sum 4sum问题

一、twoSum问题

  • 力扣上的 twoSum 问题,题目要求返回的是索引,这里编一道 twoSum 题目,不要返回索引,返回元素的值

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

思路:
我们可以先对 nums 排序,然后利用前面写过的左右双指针技巧,从两端相向而行就行了:

vector<int> twoSum(vector<int>& nums, int target) {
    // 先对数组排序
    sort(nums.begin(), nums.end());
    // 左右指针
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        // 根据 sum 和 target 的比较,移动左右指针
        if (sum < target) {
            lo++;
        } else if (sum > target) {
            hi--;
        } else if (sum == target) {
            return {nums[lo], nums[hi]};
        }
    }
    return {};
}

这样就可以解决这个问题,不过我们要继续魔改题目,把这个题目变得更泛化,更困难一点:

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

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

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

思路:

  • 基本思路肯定还是排序加双指针:

代码实现:

vector<vector<int>> twoSumTarget(vector<int>& nums, int target {
    // 先对数组排序
    sort(nums.begin(), nums.end());
    vector<vector<int>> res;
    int lo = 0, hi = nums.size() - 1;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        // 根据 sum 和 target 的比较,移动左右指针
        if      (sum < target) lo++;
        else if (sum > target) hi--;
        else {
            res.push_back({lo, hi});
            lo++; hi--;
        }
    }
    return res;
}

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

出问题的地方在于 sum == target 条件的 if 分支,当给 res 加入一次结果后,lo 和 hi 不应该改变 1 的同时,还应该跳过所有重复的元素
在这里插入图片描述
所以,可以对双指针的while循环做出如下修改

while (lo < hi) {
    int sum = nums[lo] + nums[hi];
    // 记录索引 lo 和 hi 最初对应的值
    int left = nums[lo], right = nums[hi];
    if (sum < target)      lo++;
    else if (sum > target) hi--;
    else {
        res.push_back({left, right});
        // 跳过所有重复的元素
        while (lo < hi && nums[lo] == left) lo++;  // lo++
        while (lo < hi && nums[hi] == right) hi--;
    }
}

这样就可以保证一个答案只被添加一次,重复的结果都会被跳过,可以得到正确的答案。不过,受这个思路的启发,其实前两个 if 分支也是可以做一点效率优化,跳过相同的元素:

vector<vector<int>> twoSumTarget(vector<int>& nums, int target) {
    // nums 数组必须有序
    sort(nums.begin(), nums.end());
    int lo = 0, hi = nums.size() - 1;
    vector<vector<int>> res;
    while (lo < hi) {
        int sum = nums[lo] + nums[hi];
        int left = nums[lo], right = nums[hi];
        if (sum < target) {
            while (lo < hi && nums[lo] == left) lo++; // 满足条件即可,nums[lo] == left 
        } else if (sum > target) {
            while (lo < hi && nums[hi] == right) hi--;
        } else {
            res.push_back({left, right});
            while (lo < hi && nums[lo] == left) lo++;  // 
            while (lo < hi && nums[hi] == right) hi--;
        }
    }
    return res;
}

这样,一个通用化的 twoSum 函数就写出来了,请确保你理解了该算法的逻辑,我们后面解决 3Sum 和 4Sum 的时候会复用这个函数。

这个函数的时间复杂度非常容易看出来,双指针操作的部分虽然有那么多 while 循环,但是时间复杂度还是 O(N),而排序的时间复杂度是 O(NlogN),所以这个函数的时间复杂度是 O(NlogN)。

Leetcode 1 两数之和

题目描述
在这里插入图片描述

/*
*   题目描述:
*   给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target的那两个整数,
*   并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
    你可以按任意顺序返回答案。
* */
public class twoSum {
    public static void main(String[] args) {
        /*
        *   java中数组是不能直接传入的,需要进行初始化,
        * */
        int[] arr = new int[]{2, 7, 11, 15};
        /*
        *   toString() 方法可以用作将一个地址形式的数组打印成真正的数组形式
        *   - toString()就是为了打印一个数组,
         * */
        System.out.println(Arrays.toString(twoSum(arr, 9)));
    }
    public static int[] twoSum(int[] nums, int target){
        /*
         *   我们可以创建一个哈希表,对于每一个x,我们首先查询哈希表中是否存在 target - x
         *   然后将x插入到哈希表中, 即可保证不会让x和自己匹配
         * */
        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        for (int i = 0; i < nums.length; i++){
            if (hashtable.containsKey(target - nums[i])){
                /*
                *   因为最后也是返回一个数组,则有,包含的话直接返回即可
                *   - return new int[]{} {}中直接返回需要的内容即可
                *   - 哈希函数中,通过键来返回值,通过get(键)
                * ,
                * */
                return new int[]{hashtable.get(target - nums[i]), i};  // 返回一个一维数组
            }
            /*
            *   把数组中的元素和对应的数组下标放入到哈希表当中
            *   - 传入到数组中使用的方法是put()
            * */
            hashtable.put(nums[i], i);
        }
        return new int[0]; // 返回一个空数组
    }
}

二、threeSum问题

Leetcode 15 三数之和

题目描述:
在这里插入图片描述
题目就是让我们找 nums 中和为 0 的三个元素,返回所有可能的三元组(triple),函数签名如下:

vector<vector<int>> threeSum(vector<int>& nums);

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

vector<vector<int>> threeSum(vector<int>& nums) {
    // 求和为 0 的三元组
    return threeSumTarget(nums, 0);
}

vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
    // 输入数组 nums,返回所有和为 target 的三元组
}

这个问题怎么解决呢?很简单,穷举呗。现在我们想找和为 target 的三个数字,那么对于第一个数字,可能是什么?nums 中的每一个元素 nums[i] 都有可能!

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

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

/* 从 nums[start] 开始,计算有序数组
 * nums 中所有和为 target 的二元组 */
vector<vector<int>> twoSumTarget(
    vector<int>& nums, int start, int target) {
    // 左指针改为从 start 开始,其他不变
    int lo = start, hi = nums.size() - 1;
    vector<vector<int>> res;
    while (lo < hi) {
        ...
    }
    return res;
}

/* 计算数组 nums 中所有和为 target 的三元组 */
vector<vector<int>> threeSumTarget(vector<int>& nums, int target) {
    // 数组得排个序
    sort(nums.begin(), nums.end());
    int n = nums.size();
    vector<vector<int>> res;
    // 穷举 threeSum 的第一个数
    for (int i = 0; i < n; i++) {
        // 对 target - nums[i] 计算 twoSum
        vector<vector<int>> 
            tuples = twoSumTarget(nums, i + 1, target - nums[i]);
        // 如果存在满足条件的二元组,再加上 nums[i] 就是结果三元组
        for (vector<int>& tuple : tuples) {
            tuple.push_back(nums[i]);
            res.push_back(tuple);
        }
        // 跳过第一个数字重复的情况,否则会出现重复结果
        while (i < n - 1 && nums[i] == nums[i + 1]) i++;
    }
    return res;
}

上面是C++解题demo

使用Java来进行解题

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
    //     // 对数组进行排序
    //     Arrays.sort(nums);
    //     int n = nums.length;
    //     // 创建一个二维数组来接收
    //     List<List<Integer>> res = new ArrayList<List<Integer>>();
    //     // 穷举threeSum中的第一个数
    //     for(int i = 0; i < n; i++){
    //         // 对target - nums[i] 计算twoSum
    //         List<Integer> tuples = twoSum(nums, i+1, 0-nums[i]);
    //         for(List<Integer> tuple : tuples){
    //             tuple.add(nums[i]);
    //             res.add(tuple);
    //         }
    //         // 为了避免重复,跳过第一个数字重复的情况
    //         while(i < n-1 && nums[i] == nums[i+1]) i++;
    //     }
    //     return res;
    // }
    // public List<Integer> twoSum(int[] nums, int start, int target){
    //     // 创建一个一维数组存入符合条件的数组元素
    //     List<Integer> res = new ArrayList<>();
    //     Arrays.sort(nums);
    //     int lo = start, hi = nums.length - 1;
    //     while(lo < hi){
    //         int sum = nums[lo] + nums[hi];
    //         // 要防止元素重复,所以要记录当前下标对应的元素值,做比较
    //         int left = nums[lo], right = nums[hi];
    //         if(sum > target){
    //             while(lo < hi && nums[hi] == right) hi--;
    //         }else if(sum < target){
    //             while(lo < hi && nums[lo] == left) lo++;
    //         }else{
    //             Collection.addAll(res, lo, hi);
    //         }
    //     }
        List<List<Integer>> res = new ArrayList<>();
        int n = nums.length;
        if(nums == null || n < 3) return res;
        Arrays.sort(nums);
        for(int i = 0; i < n; i++){
            // 如果当前数字大于0,总和肯定大于0,当不进入本次循环的时候,break以及continue的用法可以大大节省程序运行的时间!!!
            if(nums[i] > 0) break;
            // 每一个位置上的数字都需要进行去重操作
            if(i > 0 && nums[i] == nums[i-1]) continue;
            int lo = i+1, hi = n-1;
            while(lo < hi){
                int sum = nums[i] + nums[lo] + nums[hi];
                if(sum == 0){
                    // 二维数组里面添加一个一维数组
                    res.add(Arrays.asList(nums[i], nums[lo], nums[hi]));
                    // 去重
                    while(lo < hi && nums[lo] == nums[lo+1]) lo++;
                    while(lo < hi && nums[hi] == nums[hi-1]) hi--;
                    lo++;
                    hi--;
                }else if(sum < 0){
                    lo++;
                }else{
                    hi--;
                }
            }
        }
        return res;
    }
}

注意:

  • 类似 twoSum,3Sum 的结果也可能重复,比如输入是 nums = [1,1,1,2,3], target = 6,结果就会重复。
  • 关键点在于,不能让第一个数重复,至于后面的两个数,我们复用的 twoSum 函数会保证它们不重复。所以代码中必须用一个 while 循环来保证 3Sum 中第一个元素不重复。
  • 至此,3Sum 问题就解决了,时间复杂度不难算,排序的复杂度为 O(NlogN),twoSumTarget 函数中的双指针操作为 O(N),threeSumTarget 函数在 for 循环中调用 twoSumTarget 所以总的时间复杂度就是 O(NlogN + N^2) = O(N^2)。

Java中Vector类

Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的:

  • Vector 是同步访问的。
  • Vector 包含了许多传统的方法,这些方法不属于集合框架。

Vector 主要用在事先不知道数组的大小,或者只是需要一个可以改变大小的数组的情况。

Vector 类支持 4 种构造方法。

  • 第一种构造方法创建一个默认的向量,默认大小为 10:
Vector()
  • 第二种构造方法创建指定大小的向量。
Vector(int size)
  • 第三种构造方法创建指定大小的向量,并且增量用 incr 指定。增量表示向量每次增加的元素数目。
Vector(int size,int incr)
  • 第四种构造方法创建一个包含集合 c 元素的向量:
Vector(Collection c)

Vector定义的方法

向队列中一次性添加多个元素

List<Integer> list = new ArrayList<>();
list.add(1);
// list.addAll({1, 2, 3});  错误,添加多个元素使用Collections
// System.out.println(list);
Collections.addAll(list, 1, 2, 3, 4);
System.out.println(list); // [1, 1, 2, 3, 4]

在这里插入图片描述

4Sum问题

Leetcode 18题四数之和
在这里插入图片描述
函数签名:

vector<vector<int>> fourSum(vector<int>& nums, int target);

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

vector<vector<int>> fourSum(vector<int>& nums, int target) {
    // 数组需要排序
    sort(nums.begin(), nums.end());
    int n = nums.size();
    vector<vector<int>> res;
    // 穷举 fourSum 的第一个数
    for (int i = 0; i < n; i++) {
        // 对 target - nums[i] 计算 threeSum
        vector<vector<int>> 
            triples = threeSumTarget(nums, i + 1, target - nums[i]);
        // 如果存在满足条件的三元组,再加上 nums[i] 就是结果四元组
        for (vector<int>& triple : triples) {
            triple.push_back(nums[i]);
            res.push_back(triple);
        }
        // fourSum 的第一个数不能重复
        while (i < n - 1 && nums[i] == nums[i + 1]) i++;
    }
    return res;
}

/* 从 nums[start] 开始,计算有序数组
 * nums 中所有和为 target 的三元组 */
vector<vector<int>> 
    threeSumTarget(vector<int>& nums, int start, int target) {
        int n = nums.size();
        vector<vector<int>> res;
        // i 从 start 开始穷举,其他都不变
        for (int i = start; i < n; i++) {
            ...
        }
        return res;

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

100Sum问题(nSum问题)

统一出一个 nSum 函数:

/* 注意:调用这个函数之前一定要先给 nums 排序 */
vector<vector<int>> nSumTarget(
    vector<int>& nums, int n, int start, int target) {

    int sz = nums.size();
    vector<vector<int>> res;
    // 至少是 2Sum,且数组大小不应该小于 n
    if (n < 2 || sz < n) return res;
    // 2Sum 是 base case
    if (n == 2) {
        // 双指针那一套操作
        int lo = start, hi = sz - 1;
        while (lo < hi) {
            int sum = nums[lo] + nums[hi];
            int left = nums[lo], 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 {
                res.push_back({left, right});
                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 < sz; i++) {
            vector<vector<int>> 
                sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
            for (vector<int>& arr : sub) {
                // (n-1)Sum 加上 nums[i] 就是 nSum
                arr.push_back(nums[i]);
                res.push_back(arr);
            }
            while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
        }
    }
    return res;
}

解释:

  • 看起来很长,实际上就是把之前的题目解法合并起来了,n == 2 时是 twoSum 的双指针解法,n > 2时就是穷举第一个数字,然后递归调用计算 (n-1)Sum,组装答案。
  • 需要注意的是,调用这个 nSum 函数之前一定要先给 nums 数组排序,因为 nSum 是一个递归函数,如果在 nSum 函数里调用排序函数,那么每次递归都会进行没有必要的排序,效率会非常低。

比如说:现在我们写 LeetCode 上的 4Sum 问题:

vector<vector<int>> fourSum(vector<int>& nums, int target) {
    sort(nums.begin(), nums.end());
    // n 为 4,从 nums[0] 开始计算和为 target 的四元组
    return nSumTarget(nums, 4, 0, target);
}

比如说:LeetCode 的 3Sum 问题,找 target == 0 的三元组

vector<vector<int>> threeSum(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    // n 为 3,从 nums[0] 开始计算和为 0 的三元组
    return nSumTarget(nums, 3, 0, 0);        
}

那么,如果让你计算 100Sum 问题,直接调用这个函数就完事儿了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值