四数之和(详细题解:双指针+排序)

2023-04-12 14:31:38 浏览数 (1)

18. 四数之和

难度中等1502

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] nums[b] nums[c] nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

代码语言:javascript复制
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

代码语言:javascript复制
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109

解题思路:排序 双指针

这道题其实就是 15. 三数之和 的拓展,本质上还是一样的,只不过剪枝和去重的细节又多了一点!

​ 首先一开始还是排序,这个问题我们已经在三数之和那道题中说过了,具体的可以去翻阅笔记查看!

​ 接着就是遍历 nums,因为这次是四个数,双指针分别占了两个,所以我们在最外层的循环还需要套两层,比三数之和多了一层循环,最重要的是多了一层循环,也就是多了一个剪枝和去重工作!但是大体步骤和三数之和是类似的!

1、先来讲讲最外层这个 i 的剪枝和去重工作:

​ 这道题三数之和不太一样,这道题要求四个数的和不能超过 target,那么我们在最外层的剪枝判断中能不能说直接 nums[i] > target 就剪枝呢?肯定是不行的,因为 target 可能是负数,并且 nums[i] 以及 nums[i] 后面的数也可能是负数,这个时候一个数加上负数,它们的和反而是会变小的,所以 不能直接判断是否 nums[i] > target 就剪枝!举个例子,比如 {-4, -1, 0, 0} 数组中我们要找的是和为 -5 的,那么这个数组刚刚好就是一个解,但是如果我们如果直接判断 nums[i] > target 也就是 -4 > -5 后剪枝,那么就错过了这个解,那么就错了!

​ 所以最外层的剪枝工作是需要修改一下的,我们还需要判断 nums[i] 是否大于等于0,因为我们的数组是排序过的,这样子就能避免 nums[i] 后面加的数是负数,所以正确的剪枝判断是: if(nums[i] > target && nums[i] >= 0) break;

​ 接下来就是最外层的去重操作,这个和三数之和是一模一样的,只要判断 nums[i]nums[i - 1] 是否相同,是的话就说明重复了,直接 continue,除此之外 还需要注意边界问题就是需要让 i > 0 防止 nums[i -1] 索引越界!具体的原因可以参考三数之和的笔记!

2、接下来讲一下第二层 j 的剪枝和去重工作:

​ 首先 j 肯定是通过最外层的 i 1 来开始遍历的,所以对于去重工作,都是类似的,只是这里不是 nums[j] > 0而是 nums[j] > i 1 来进行防止索引越界,并且只要 nums[j] == nums[j - 1] 我们就直接 continue 即可!

​ 对于第二层的剪枝,不只是单单的判断 nums[j] > target 就剪枝,因为有可能我们最外层的 nums[i] 就是一个负数,而 nums[i] nums[j] 之后得到的数比 target 还小,那么这就不能剪枝了,所以我们要考虑到最外层的情况!

正确的剪枝操作:if(nums[i] nums[j] > target && nums[i] >= 0 && nums[j] >= 0) continue;

​ 上述剪枝操作也可以进行一下化简:if(nums[i] nums[j] > target && nums[i] >= 0) continue;

​ 这个化简就是简单的数学问题了,要求 nums[i] >= 0,因为数组的有序的,所以 nums[j] 肯定也是 >= 0 的,所以就没必要判断 j了!

3、最后说一下双指针的移动

​ 这个大概操作和三数之和是一样的!但是有一些细节问题,就是直接是三个数与 target 比较,现在要变成四个数!

​ 除此之外,我们四个数相加,对于这道题的数据来说是会发生溢出的,所以我们在相加的时候需要将它们的类型转化为 long 类型以上才行,而对于类型转化表达式,我们只需要对其中一个变量进行类型转化,其它的三个变量会根据不同类型进行隐式类型转化的,也就是一个类型是 long,其它类型是 int 的话,最后相加的结果还是 long!这样子就不会溢出了!

​ 其它问题就和三数之和是一样的!

代码语言:javascript复制
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        sort(nums.begin(), nums.end()); // 别忘了排序!

        vector<vector<int>> vv;
        int n = nums.size();
        for(int i = 0; i < nums.size();   i)
        {
            // 进行一级剪枝和一级去重
            // 剪枝中注意如果target是负数的话且nums[i]>target,那么直接不能剪枝
            // 因为虽然nums[i]>target,但是nums[i]加上nums[j]不一定比target大,所以要排除nums[i]小于0的情况!
            if(nums[i] > target && nums[i] >= 0)
                break;
            if(i > 0 && nums[i] == nums[i - 1])
                continue;

            for(int j = i   1; j < nums.size();   j)
            {
                // 进行二级剪枝和二级去重
                // 注意这里剪枝的时候不只是判断nums[j]>target,因为有可能nums[i]是负数,这样子它们两个数相加之后可能会比target小
                // 这里写成if(nums[i]   nums[j] > target && nums[i] >= 0) 也是可以的,数学问题!
                if(nums[i]   nums[j] > target && nums[i]   nums[j] >= 0)
                    break;

                // 去重时候如果前一个是i的话,那么相当于j是i这一轮的第一遍执行,所以不需要continue
                if(j > i   1 && nums[j] == nums[j - 1])
                    continue;

                int left = j   1, right = n - 1;
                while(left < right)
                {
                    // 四个数加起来可能会溢出,所以要先变成long类型
                    if((long) nums[i]   nums[j]   nums[left]   nums[right] > target)
                        right--;
                    else if((long) nums[i]   nums[j]   nums[left]   nums[right] < target)
                        left  ;
                    else
                    {
                        vv.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});

                        // left和right的去重
                        while(left < right && nums[right] == nums[right - 1])
                            right--;
                        while(left < right && nums[left] == nums[left   1])
                            left  ;
                        
                        // 别忘了再更新一次left和right才是不重复的那个值
                        left  ;
                        right--;
                    }
                }
            }
        }
        return vv;
    }
};

0 人点赞