算法图解-四数之和

495 阅读4分钟

四数之和

给你一个由 n 个整数组成的数组 nums,和一个目标值 target

请你找出和为target的所有四元组组合

注:四元组中的数对应nums中的索引不能重复,且四元组组合也不能重复

0 <= a, b, c, d < n

a、b、c 和 d 互不相同

nums[a] + nums[b] + nums[c] + nums[d] == target

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

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

暴力法

暴力法枚举所有情况,并将每种组合的数hash去重

  • Javascript
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function (nums, target) {
  const result = [];
  const n = nums.length;
  let sum;
  const set = new Set();
  function hash() {
    return [...arguments]
      .sort((a, b) => a - b)
      .join(',');
  }
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      for (let k = j + 1; k < n; k++) {
        for (let l = k + 1; l < n; l++) {
          sum = nums[i] + nums[j] + nums[k] + nums[l];
          if (sum !== target) continue;
          const hashValue = hash(nums[i], nums[j], nums[k], nums[l]);
          if (set.has(hashValue)) continue;
          result.push([nums[i], nums[j], nums[k], nums[l]]);
          set.add(hashValue);
        }
      }
    }
  }
  return result;
};
  • cpp
class Solution {
public:
    string hash(vector<int> nums){
        string s;
        for(int val:nums){
            s+=to_string(val);
            s+=',';
        }
        return s;
    }

    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        int n = nums.size();
        int sum;
        set<string> s;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    for (int l = k + 1; l < n; l++) {
                        sum = nums[i] + nums[j] + nums[k] + nums[l];
                        if (sum != target) continue;
                        vector<int> t={nums[i],nums[j],nums[k],nums[l]};
                        sort(t.begin(),t.end());
                        string hashValue = hash(t);
                        if (s.find(hashValue)!=s.end()) continue;
                            result.push_back(t);
                            s.insert(hashValue);
                        }
                    }
                }
        }
        return result;
    }
};

最终我们 js 代码的提交不负众望的过了287个用例,只差两个

时间复杂度:O(n^4) 空间复杂度:O(n^4)

解法一:排序+双指针

之前我们已解决过三数之和的问题,在这里我们将问题简化一下,复用下三数之和的解法

三数之和

将四数之和变成 固定其中一个数 与三数之和 等于 target 的问题

用 [1,0,-1,0,2,-2],target=0 举例

  1. 固定 1,剩下求 [0,-1,0,2,-1] 中三数之和为 -1 的组合

  2. 固定 0,剩下求 [1,-1,0,2,-2] 中三数之和为 0 的组合

  • JavaScript
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function (nums, target) {
  const result = [];
  const n = nums.length;
  nums = nums.sort((a, b) => a - b);
  let sum;
  let l, r;

  for (let i = 0; i < n; i++) {
    if (i > 0 && nums[i] === nums[i - 1]) continue;
    for (let j = i + 1; j < n; j++) {
      if (j > i + 1 && nums[j] === nums[j - 1]) continue;
      l = j + 1;
      r = n - 1;
      while (l < r) {
        sum = nums[i] + nums[j] + nums[l] + nums[r];
        if (sum < target) {
          l++;
        } else if (sum > target) {
          r--;
        } else {
          result.push([nums[i], nums[j], nums[l], nums[r]]);
          l++;
          r--;
          // 去重
          while (l < r && nums[l] === nums[l - 1]) {
            l++;
          }
          // 去重
          while (r >= 0 && nums[r] === nums[r + 1]) {
            r--;
          }
        }
      }
    }
  }
  return result;
};

  • C++
class Solution {
public:
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        int n = nums.size();
        sort(nums.begin(),nums.end());
        long sum;
        int l,r;
        for (int i = 0; i < n; i++) {
            // 去重
            if(i>0 && nums[i]==nums[i-1])continue;
            for (int j = i + 1; j < n; j++) {
                // 去重
                if(j>i+1 && nums[j]==nums[j-1])continue;
                l=j+1,r=n-1;
                while(l<r){
                    sum = (long)nums[i] + (long)nums[j] + (long)nums[l] + (long)nums[r];
                    if(sum<target){
                        l++;
                    }else if(sum>target){
                        r--;
                    }else{
                        vector<int> t={nums[i],nums[j],nums[l],nums[r]};
                        result.push_back(t);
                        l++;
                        r--;
                        // 去重
                        while(l<r&&nums[l]==nums[l-1]){
                            l++;
                        }
                        // 去重
                        while(r>=0&&nums[r]==nums[r+1]){
                            r--;
                        }
                    }
                }
            }
        }
        return result;
    }
};

时间复杂度:O(n^3)

空间复杂度:取决于排序算法

解法二:DFS + 剪枝

我们直接使用深度优先搜索枚举所有四数组合的话,在数据集达到一定量就会超时,因此我们需要通过剪枝优化算法的时间复杂度

先看一下深度优先搜索的过程

我们的树中,从上往下的路径便是枚举出来的组合,例如:[-2,-1,0,0] [-2,0,1,2]

数据集需要先排序,便于剪枝

用 [-2,-1,0,0,1,2] target=0 举例子,看看剪枝过程

  • 去重剪枝

青色节点组合跟黄色节点组合是重复的,因此青色节点组合应该要剪掉

并且观察到是数组中重复元素导致的,所以在代码里可以通过判断重复节点来跳过搜索

  • 短路径剪枝

黄色节点的路径都不满足 4 个元素,因此要剪掉

  • 最大值剪枝

数组是有序的,我们可以通过数组最后一项得知我们能组合相加的最大值,来判断往后继续搜索的必要性

例如 target 如果为 10 的时候,我们用例可能组合成的最大值是 8 ,因此整颗数都被剪了

  • 最小值剪枝

target 为 -4 的时候,从黄色节点开始,能组合成的最小值是 加不到 -4,因此要剪掉

最小值剪枝后,如下图,我们的 dfs 时间复杂度大大降低

上代码:

  • JavaScript
/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[][]}
 */
var fourSum = function(nums, target) {
    const t=[]
    const len=nums.length
    const result=[]
    // 排序
    nums=nums.sort((a,b)=> a-b)
    function dfs(idx,n,target){
        if(!n){
            // 四数和=target,添加结果
            if(!target){
                result.push([...t])
            }
            return
        }
        // 短路径剪枝
        if(len-idx<n){
            return
        }
        // 最小值剪枝
        if(target<n*nums[idx]){
            return
        }
        // 最大值剪枝
        if(target>n*nums[len-1]){
            return
        }
        for(let i=idx;i<len;i++){
            // 去重剪枝
            if(i>idx && nums[i]===nums[i-1])continue
            t.push(nums[i])
            dfs(i+1,n-1,target-nums[i])
            t.pop()
        }
    }
    dfs(0,4,target)
    return result
};
  • C++
class Solution {
public:
    
    void dfs(int idx,int n,int target,vector<vector<int>> &result,vector<int> &path,vector<int> &nums){
        if(!n){
            // 结果添加
            if(!target){
                vector<int> t(path);
                result.push_back(t);
            }
            return;
        }
        // 短路径剪枝
        if(nums.size()-idx<n){
            return;
        }
        // 最大值剪枝
        if(target>(long)nums[nums.size()-1]*n){
            return;
        }
        // 最小值剪枝
        if(target<(long)nums[idx]*n){
            return;
        }
        for(int i=idx;i<nums.size();i++){
            // 去重剪枝
            if(i>idx && nums[i]==nums[i-1])continue;
            path.push_back(nums[i]);
            dfs(i+1,n-1,target-nums[i],result,path,nums);
            path.pop_back();
        }
    }
    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        vector<vector<int>> result;
        vector<int> path;
        sort(nums.begin(),nums.end());
        dfs(0,4,target,result,path,nums);
        return result;
    }
};