【LeetCode】四数之和

97 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第18天,点击查看活动详情

四数之和

原题:leetcode-cn.com/problems/4s…

描述

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d 使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

注意: 答案中不可以包含重复的四元组。

难度

中等

示例

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

思路

本题思路和三数之和解题思路一样,但可以使用一种通用的解法,用来解决四数之和、五数之和和六数之和等等。

三数之和的解题思路如下:

为了保证结果不重复。我们先将数组按照从小到大排序,为了不重复,需满足以下条件:

  • 第二个元素不小于第一个元素
  • 第三个元素不小于第二个元素

先枚举所有的元素,随后使用两个指针,left 表示左指针,right 表示右指针,从大于第一个元素的数组中找出所有两个数之和等于 target - nums[i] 的两个元素,再和第一层循环的元素进行配对,组成三个元素为一组的数组放入结果集中。

第一层循环时需要去掉和上一次循环相同的元素,当从剩余的数组中找到两个元素之后,从 left 指针开始向后寻找一个不等于当前 left 指针指向的元素的下标,从 right 指针开始向前寻找一个不等于当前 right 指针指向的元素的下标,然后继续循环,直到 left 大于 right。

通过三数之和可以发现,第一个数就是枚举数组的所有元素,然后从剩下的数组中寻找两个数之和。我们将三数之和拆分成一层循环 + 两数之和,求出所有不重复的两数之和放入集合中,然后和第一层循环的每个元素做笛卡尔积,组成所有三数之和等于 target 的集合。

如果要计算四数之和,拆分成两层循环 + 两数之和。第一层循环枚举所有元素,然后第二次循环从第一层循环的下一个元素开始枚举所有元素,设第一层循环时下标为 i,第二层循环时下表为 j,第二层循环要求剩下的三数之和为 target - nums[i],最后求出和为 target - nums[i] - nums[j] 的两个数。示例代码如下:

for (i = 0; i < nums.length; i++) {
  for (j = i + 1; j < nums.length; j++) {
    twoSum(nums, target - nums[i] - nums[j]);
  }
}

最后将 num[i], nums[j] 和剩下的两个数做笛卡尔积,得到的集合就是四数之和。

为了达到通用的设计,我们采用递归的方式,递归函数设计如下:

nSum(int[] nums, int start, int end, int target, int n, int[] records, int[] res)
  • start:当前起始下标
  • end:当前结束下标
  • target:目标和
  • n:计算第几个数之和
  • records:记录第几个数是多少
  • res:最终的结果

两数之和的函数设计如下:

int[][] twoSum(int nums, int start, int end, int target)

返回结果为所有两数之和的集合。

递归调用 nSum , 当 n > 2 时,迭代剩余的元素,当 n < 2 时,调用 twoSum 计算两数之和,判断返回结果,如果没有值,表示没有满足目标和的四数,否则处理结果,从 records 中取出前两个数和剩下的所有两个数做笛卡尔积,最后将结果放入 res 中。

首次调用 nSum 时,n 传入 4,以后每调用一次,递减:

nSum(nums, 0, len(nums) - 1, target, 4, records)

nSum 函数伪代码实现如下:

nSum(int[] nums, int start, int end, int target, int n, int[] records, int[] res) {
  if (n <= 2) {
    // 计算两数之和
    twoSum(nums, start, end, target);
    // 处理结果
    
    retrun;
  }
  int i = start
  for (;i <= end; i++) {
    if (i + 1 <end) {
      nSum(nums, i + 1, end, target - nums[i], n - 1, records, res);
    }
  }
}

代码

Rust

pub struct Solution {}
​
impl Solution {
    pub fn four_sum(nums: Vec<i32>, target: i32) -> Vec<Vec<i32>> {
        let mut nums = nums;
        nums.sort();
        let mut res: Vec<Vec<i32>> = Vec::new();
        let mut records = [i32::MIN; 4];
        let len = nums.len();
        if len == 0 {
            return res
        }
        Solution::n_sum(&mut nums, 0, len - 1, target, 4, &mut records, &mut res);
        res
    }
​
    /// 计算 n 个数之和
    fn n_sum(nums: &Vec<i32>, start: usize, end: usize, target: i32, n: u32, records: &mut [i32], res: &mut Vec<Vec<i32>>) {
        let record_len = records.len();
        if n <= 2 {
            // 计算两数之和
            let results: Vec<Vec<i32>> = Solution::two_sum(nums, start, end, target);
            // 存在两数之和等于目标值
            if results.len() > 0 {
                // 循环遍历结果, 将结果添加到最后的结果中
                for result in results {
                    records[record_len - 2] = result[0];
                    records[record_len - 1] = result[1];
​
                    let mut list = Vec::new();
                    for record in records.iter() {
                        list.push(*record);
                    }
                    res.push(list);
                }
            }
            return;
        }
        let mut i = start;
        while i <= end {
            // 如果元素重复, 跳过
            if nums[i] == records[record_len - n as usize] {
                i += 1;
                continue;
            }
            // 修改当前第 n 个数
            records[record_len - n as usize] = nums[i];
            if i + 1 < end {
                // 继续计算第 n - 1 个数之和
                Solution::n_sum(nums, i + 1, end, target - nums[i], n - 1, records, res);
            }
            // 表示下一次重新计算 n 数之和, 将向量元素重置
            if n as usize == record_len {
                for j in 1..record_len {
                    records[j] = i32::MIN;
                }
            }
            i += 1;
        }
    }
​
    /// 计算两数之和
    fn two_sum(nums: &Vec<i32>, start: usize, end: usize, target: i32) -> Vec<Vec<i32>> {
        let mut results: Vec<Vec<i32>> = Vec::new();
        let (mut left, mut right) = (start, end);
        while left < right {
            let left_val = nums[left];
            let right_val = nums[right];
            let sum = left_val + right_val;
            // 如果相等,将元素放入集合
            if sum == target {
                let mut list = Vec::new();
                list.push(left_val);
                list.push(right_val);
                results.push(list);
​
                // 如果元素相同指针后移
                while left < right && nums[left] == left_val {
                    left += 1;
                }
                // 如果元素相同指针前移
                while left < right && nums[right] == right_val {
                    right -= 1;
                }
                continue;
            }
            // 和小于目标值,从右边寻找下一个较大的值
            if sum < target {
                left += 1;
                continue;
            }
            // 和大于目标值,从左边寻找下一个较小的值
            right -= 1;
        }
        results
    }
}
#[test]
fn test_four_sum() {
    let nums: Vec<i32> = vec![1, 0, -1, 0, -2, 2];
    let target = 0;
    println!("nums = {:?}, target = {}", nums.clone(), target);
​
    let sum = Solution::four_sum(nums, target);
    println!("{:?}", sum);
}

运行结果:

nums = [1, 0, -1, 0, -2, 2], target = 0
[[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]

Go

var res [][]int
​
func fourSum(nums []int, target int) [][]int {
    res = make([][]int, 0)
​
    sort.Ints(nums)
    // 记录当前第 n 个数
    records := make([]int, 4)
    for i := 0; i < len(records); i++ {
        records[i] = math.MinInt32
    }
    nSum(nums, 0, len(nums) - 1, target, 4, records)
    return res
}
​
// 计算 n 个数之和
func nSum(nums []int, start, end, target, n int, records []int) {
    recordLen := len(records)
    if n <= 2 {
        // 计算两数之和
        results := twoSum(nums, start, end, target)
        // 存在两数之和等于目标值
        if len(results) > 0 {
            // 循环遍历结果, 将结果添加到最后的结果中
            for i := 0; i < len(results);i++ {
                records[recordLen - 2] = results[i][0]
                records[recordLen - 1] = results[i][1]
                res = append(res, append([]int{}, records...))
            }
        }
        return
    }
    i := start
    for ; i <= end; i++ {
        // 如果元素重复, 跳过
        if nums[i]  == records[recordLen - n] {
            continue
        }
​
        // 修改当前第 n 个数
        records[recordLen - n] = nums[i]
        if i + 1 < end {
            // 继续计算第 n - 1 个数之和
            nSum(nums, i + 1, end, target - nums[i], n - 1, records)
        }
​
        // 表示下一次重新计算 n 数之和, 将数组元素重置
        if n == recordLen {
            for j := 1; j < recordLen; j++ {
                records[j] = math.MinInt32
            }
        }
    }
}
​
// 计算两数之和
func twoSum(nums []int, start, end, target int) [][]int {
    var results [][]int
    left, right := start, end
    sum := 0
    for left < right {
        leftVal := nums[left]
        rightVal := nums[right]
        sum = leftVal + rightVal
        // 如果相等,将元素放入切片
        if sum == target {
            results = append(results, []int{leftVal, rightVal})
​
            // 如果元素相同指针后移
            for left < right && nums[left] == leftVal {
                left++
            }
            // 如果元素相同指针前移
            for left < right && nums[right] == rightVal {
                right--
            }
            continue
        }
        // 和小于目标值,从右边寻找下一个较大的值
        if sum < target {
            left++
            continue
        }
        // 和大于目标值,从左边寻找下一个较小的值
        right--
    }
    return results
}
func TestFourSum(t *testing.T) {
    nums := []int{1, 0, -1, 0, -2, 2}
    target := 0
    t.Logf("nums = %v, target = %v\n", nums, target)
​
    sum := fourSum(nums, target)
    t.Log(sum)
}

运行结果:

nums = [1 0 -1 0 -2 2], target = 0
[[-2 -1 1 2] [-2 0 0 2] [-1 0 0 1]]

Java

public class Main {
​
​
    public static List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
​
        Arrays.sort(nums);
        int[] records = new int[4];
        Arrays.fill(records, Integer.MIN_VALUE);
        nSum(nums, 0, nums.length - 1, target, 4, records, res);
        return res;
    }
​
    /**
     * 计算 n 个数之和
     */
    public static void nSum(int[] nums, int start, int end, int target, int n, int[] records, List<List<Integer>> res) {
        int recordLen = records.length;
        if (n <= 2) {
            // 计算两数之和
            List<List<Integer>> results = twoSum(nums, start, end, target);
            // 存在两数之和等于目标值
            if (results.size() > 0) {
                // 循环遍历结果, 将结果添加到最后的结果中
                for (List<Integer> result : results) {
                    records[recordLen - 2] = result.get(0);
                    records[recordLen - 1] = result.get(1);
​
                    List<Integer> list = new ArrayList<>();
                    for (int record : records) {
                        list.add(record);
                    }
                    res.add(list);
                }
            }
            return;
        }
        int i = start;
        for (; i < end; i++) {
            // 如果元素重复, 跳过
            if (nums[i]  == records[recordLen - n]) {
                continue;
            }
​
            // 修改当前第 n 个数
            records[recordLen - n] = nums[i];
            if (i + 1 < end) {
                // 继续计算第 n - 1 个数之和
                nSum(nums, i + 1, end, target - nums[i], n - 1, records, res);
            }
​
            // 表示下一次重新计算 n 数之和, 将数组元素重置
            if (n == recordLen) {
                for (int j = 1; j < recordLen; j++) {
                    records[j] = Integer.MIN_VALUE;
                }
            }
        }
    }
​
    /**
     * 计算两数之和
     */
    public static List<List<Integer>> twoSum(int[] nums, int start, int end, int target) {
        List<List<Integer>> results = new ArrayList<>();
        int left = start, right = end;
        int sum;
        while (left < right) {
            int leftVal = nums[left];
            int rightVal = nums[right];
            sum = leftVal + rightVal;
            // 如果相等,将元素放入集合
            if (sum == target) {
                List<Integer> list = new ArrayList<>();
                list.add(leftVal);
                list.add(rightVal);
                results.add(list);
​
                // 如果元素相同指针后移
                while (left < right && nums[left] == leftVal) {
                    left++;
                }
                // 如果元素相同指针前移
                while (left < right && nums[right] == rightVal) {
                    right--;
                }
                continue;
            }
            // 和小于目标值,从右边寻找下一个较大的值
            if (sum < target) {
                left++;
                continue;
            }
            // 和大于目标值,从左边寻找下一个较小的值
            right--;
        }
        return results;
    }
​
    public static void main(String[] args) {
        int[] nums = new int[]{1, 0, -1, 0, -2, 2};
        int target = 0;
        System.out.printf("nums = %s, target = %d\n", Arrays.toString(nums), target);
​
        List<List<Integer>> results = fourSum(nums, target);
        StringBuilder sb = new StringBuilder("[");
        for (List<Integer> ints : results) {
            sb.append("[");
            for (Integer in : ints) {
                sb.append(in).append(", ");
            }
            sb.deleteCharAt(sb.length() - 1);
            sb.deleteCharAt(sb.length() - 1);
            sb.append("]");
            sb.append(", ");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.deleteCharAt(sb.length() - 1);
        sb.append("]");
        System.out.println(sb.toString());
    }
}

运行结果:

nums = [1, 0, -1, 0, -2, 2], target = 0
[[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]