【LeetCode-中等】18.四数之和 - 哈希/双指针

力扣题目链接

题目要求在 nums 数组里找到四个下标不同(值可以相同)的数,使得nums[a] + nums[b] + nums[c] + nums[d] == target,同时,还要求四元组不能重复({0, 1, 2, -2} 和 {-2, 0, 1, 2} 为同一个四元组,即使他们下标可能不同),这意味着我们要考虑去重的问题。

这道题和15. 三数之和 是一个思路,基本解法就是在15. 三数之和的基础上再套一层循环。

要注意:这道题中的 target 是不确定的值,不能直接通过nums[i] > target来进行剪枝处理,存在{-4, -3, -2, -1}, target = -10 这种情况,这时候用nums[i] > target 来剪枝,就会因为 -4 > -10而无法得到正确的解。

1. 哈希法

既然涉及到去重,那么就可以尝试使用哈希法求解,大体思路为通过三层循环遍历前三个元素,通过target - nums[i] - nums[j] - nums[k] 得到第四个元素,判断第四个元素是否在哈希表中,如果在则表示已经找到一对符合要求的四元组,将这个四元组插入到结果数组中;如果不在,就将第三个元素插入到哈希表中(注意是插入第三个元素,因为第三层循环是遍历的第三个元素,可以理解为:哈希表中存入 数组中已经被遍历过但还没有找到配对数字的第三个元素,它们将作为可能的第四个元素等待后续配对)。

还要注意,每层循环中都需要添加条件来跳过相同的元素,达到去重的目的。

  1. nums 数组进行排序,这样方便进行去重和剪枝。
  2. 使用三层循环来遍历所有可能的四元组,为了避免重复的四元组,需要在每层循环中添加条件来跳过相同的元素。
    • 第一层循环遍历第一个数字nums[i]
    • 第二层循环遍历第二个数字nums[j],j 从 i + 1 开始
    • 第三层循环遍历第三个数字nums[k],k 从 j + 1 开始,同时在内部,通过d = target - nums[i] - nums[j] - nums[k]得到第四个数字,判断第四个数字是否在集合 set 中,如果在,表示已经找到一对符合要求的四元组,将这个四元组插入到结果数组中;如果不在,就将第三个元素插入到 set 中。

下面将举例帮助理解每层循环中添加的用来去重的条件是如何工作的:

  1. 第一层循环
    例:nums = {2, 2, 1, 1, 1},target = 5
    在第一个大循环 i = 0 中,已经找到符合条件的四元组 {2, 1, 1, 1} 插入到结果数组中;如果第一层循环中不进行i > 0 && nums[i] == nums[i - 1]的去重处理,将会在第二个大循环 i = 1 中将四元组 {2, 1, 1, 1} 再次插入到结果数组中,所以当第二次大循环开始后,每次需要判断当前nums[i] == nums[i - 1].
    至于为什么不用nums[i] == nums[i + 1]作为判断条件:首先,存在溢出问题,当i指向数组中最后一个元素时,i + 1将会溢出。其次,nums[i] == nums[i + 1]作为判断条件,会把出现重复元素的情况直接pass掉,例如 nums 数组不变,target = 6,{2, 2, 1, 1}这组数据当遍历到第一个 2 的时候,判断下一个也是2,那这组数据就pass了。

  2. 第二层循环
    例:nums = [2, 2, 2, 1, 1],target = 6
    第二层循环中去重的条件和第一层循环相同,可以从上例中看出,当 i = 0 时, j = 0 与 j = 1 将会得到同样的二元组{2, 2, 1, 1}

  3. 第三层循环
    例:nums = [2, 2, 1, 1, 1, 1],target = 6
    我们要在第三层循环中找到第三和第四两个元素,所以在第三层循环里有两次去重。由于可能存在像例子中那样第三第四两个元素相等的情况,所以不能仅判断有连续两个数相等时就跳过,在这次去重的过程中要判断如果是连续三次以上 k 指向的元素相等才跳过。例如例子中 i = 0,j = 1,k = 4/5找到的四元组{2, 2, 1, 1}就有这次去重条件判断出来。
    例:[-2,-1,-1,1,1,2,2], target = 0
    set.erase(d);通过将集合中已配对的元素删去,实现对第四个元素的去除。否则如例子中 i = 0,j = 1 时,k = 5 和 k = 6 将会找到同一组四元组。set.erase(d);在 k = 5 配对成功后就将 1 从 set 中删掉了, k = 6 时就不会再在 set 中找到可以配对的数。

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> res;
        if(nums.size() < 4) return res;
        sort(nums.begin(), nums.end());
        for(int i = 0; i < nums.size() - 3; i++){
        	//剪枝
            if(nums[i] > target && (nums[i] >=0 || target >= 0)){
                break;
            }
            //第一个去重
            if(i > 0 && nums[i] == nums[i - 1]){
                continue;
            }
            for(int j = i + 1; j < nums.size(); j++){
            	//剪枝
                if(nums[i] + nums[j] > target && (nums[i] + nums[j] >=0 || target >= 0)){
                    break;
                }
                //第二个元素去重
                if(j > i + 1 && nums[j] == nums[j - 1]){
                    continue;
                }
                unordered_set<long long> set;
                for(int k = j + 1; k < nums.size(); k++){
                	//第三个元素去重
                    if(k > j + 2 && nums[k] == nums[k - 1] && nums[k -1] == nums[k - 2]){
                        continue;
                    }
                    //int可能会溢出
                    long long d = (long long)target - nums[i] - nums[j] - nums[k];
                    if(set.find(d) != set.end()){
                        res.push_back({nums[i], nums[j], nums[k], (int)d});
                        //第四个元素去重
                        set.erase(d);
                    }else{
                        set.insert(nums[k]);
                    }
                }
            }
        }
        return res;
    }
};

2. 双指针法

这道题使用双指针法要比哈希法更加高效一些。
解题思路大体相同,差别主要在于第三层循环,双指针法在第三层循环中通过两个指针 left 和 right向中间移动,来寻找符合条件的第三和第四元素。

class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        sort(nums.begin(), nums.end());
        for (int k = 0; k < nums.size(); k++) {
            // 剪枝处理
            if (nums[k] > target && nums[k] >= 0) {
            	break; // 这里使用break,统一通过最后的return返回
            }
            // 对nums[k]去重
            if (k > 0 && nums[k] == nums[k - 1]) {
                continue;
            }
            for (int i = k + 1; i < nums.size(); i++) {
                // 2级剪枝处理
                if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
                    break;
                }

                // 对nums[i]去重
                if (i > k + 1 && nums[i] == nums[i - 1]) {
                    continue;
                }
                int left = i + 1;
                int right = nums.size() - 1;
                while (right > left) {
                    // nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出
                    if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) {
                        right--;
                    // nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出
                    } else if ((long) nums[k] + nums[i] + nums[left] + nums[right]  < target) {
                        left++;
                    } else {
                        result.push_back(vector<int>{nums[k], nums[i], nums[left], nums[right]});
                        // 对nums[left]和nums[right]去重
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left + 1]) left++;

                        // 找到答案时,双指针同时收缩
                        right--;
                        left++;
                    }
                }
            }
        }
        return result;
    }
};
  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值