LeetCode刷题笔记-18.四数之和(4sum)

问题描述

给定一个包含n个整数的数组nums和一个目标值target,判断nums中是否存在四个元素abc 和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]
]



题解

1.暴力解法

算法解析

使用4重循环,一次循环固定一个数,通过4次循环找到解:

  • 第一层循环固定下标为i的数,i递增直到n-1
  • 第二层循环固定下标为j = i+1的数,j递增直到n-1
  • 第三层循环固定下标为k = j+1的数,k递增直到n-1
  • 第四层循环固定下标为m = k+1的数,m递增直到n-1

同时还可以通过将数组排序,避免最后的解集中出现组合一样但是顺序不同的解.

复杂度分析
  • 时间复杂度: \(O(N^4)\), 使用4重循环,其时间复杂度为\(O(N^4)\)
  • 空间复杂度: \(O(1)\), 只需要固定的额外空间

代码实现
  • Python版
class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        n = len(nums)
        if n < 4:
            return []

        res = set() # 防止解重复
        nums.sort() # 防止出现值次序不同但值组合相同的解
        for i in range(n):
            for j in range(i+1, n):
                for k in range(j+1, n):
                    for m in range(k+1, n):
                        if nums[i] + nums[j] + nums[k] + nums[m] == target:
                            res.add(f"{nums[i]}_{nums[j]}_{nums[k]}_{nums[m]}")
                            # f字符串前缀表达式为Python3.6及之后的特性
        return [list(map(int, s.split('_'))) for s in res]


2.双指针解法

算法解析

四数之和本质上是nSum问题.该解法同之前的三数之和解法类似,同样的是固定一个数nums[i]然后求剩余三个数之和.也就变成了求三数之和问题.相比于三数之和,其具体细节如下:

  • 最外层循环,固定一个数为nums[i]:
    • nums[i] == nums[i-1](i > 0)时,即有重复的元素时,跳过重复元素.
    • nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target时,由于数组为递增数组,则i之后的不可能有四数之和小于target,直接返回结果.
    • nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target时,由于数组为递增数组,则此时的i与之后的数不可能满足四数之和大于target,跳过当前的i.
  • 第二层循环同三数之和差异不大,固定一个数为nums[j]j = i+1,但是可以同第一层循环一样进行剪枝:
    • nums[j] == nums[j-1](j > i+1)时,即有重复的元素时,跳过重复元素.
    • nums[i] + nums[j] + nums[j+1] + nums[j+2] > target时,由于数组为递增数组,则i,j之后的不可能有四数之和小于target,直接返回结果.
    • nums[i] + nums[j] + nums[n-2] + nums[n-1] < target时,由于数组为递增数组,则i,jj之后的数不可能满足四数之和大于target,跳过当前的j.
  • 第三层循环就同三数之和几乎无差异了,利用双指针法即可.此处不再赘述.
  • 特别的,利用数组元素递增的特性.上述的剪枝条件还可以进一步优化:
    • nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target可以优化为nums[i] + 3*nums[i+1] > target.
    • nums[i] + nums[n-3] + nums[n-2] + nums[n-1] < target可以优化为nums[i] + 3*nums[n-1] < target.
    • nums[i] + nums[j] + nums[j+1] + nums[j+2] > target可以优化为nums[i] + nums[j] + 2*nums[j+1] > target.
    • nums[i] + nums[j] + nums[n-2] + nums[n-1] < target可以优化为nums[i] + nums[j] + 2*nums[n-1] < target.

本题难点同三数之和一样,在于如何避免重复解.解决方法也相同,通过排序和双指针法便可轻松的解决.同时由于提供的是target,所以剪枝的方法也有所区别.由此延伸出nSum问题的解决方法递归.

参考文章及资料

LeetCode官方题解
LeetCode精选题解
LeetCode本题提交详情 Java版2ms示范代码
LeetCode国际站nSum问题递归解法

复杂度分析
  • 时间复杂度: \(O(N^3)\),一般排序算法时间复杂度为\(O(NlogN)\),而双指针遍历和和最外两层循环的时间复杂度为\(O(N^3)\)
  • 空间复杂度: \(O(logN)\)或者\(O(1)\), 一般排序算法空间复杂度为\(O(logN)\)而对于本题核心算法来说其空间复杂度为\(O(1)\)

代码实现
  • Java版
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<List<Integer>>();

        if (n < 4) return res; // 特殊情况

        Arrays.sort(nums);
        for (int i = 0; i < n-3; ++i) { // 固定一个数i,同时为保证i之后数组至少有4个元素,i应该小于n-3
            if (i > 0 && nums[i] == nums[i-1]) continue; // 跳过重复元素
            if (nums[i] + 3*nums[i+1] > target) break; // 剪枝,利用数组递增的特性
            if (nums[i] + 3*nums[n-1] < target) continue; // 剪枝,利用数组递增的特性
 
            for (int j = i+1; j < n-2; ++j) { // 固定一个数j,同理为保证j之后数组至少有3个元素,j应该小于n-2
                if (j > i+1 && nums[j] == nums[j-1]) continue; // 跳过重复元素
                if (nums[i] + nums[j] + 2*nums[j+1] > target) break; // 剪枝,利用数组递增的特性
                if (nums[i] + nums[j] + 2*nums[n-1] < target) continue; // 剪枝,利用数组递增的特性

                int left = j+1;
                int right = n-1;
                while (left < right) { // 双指针法求剩余两个目标数
                    int tmpSum = nums[i] + nums[j] + nums[left] + nums[right];
                    if (tmpSum < target) {
                        left++;
                    } else if (tmpSum > target) {
                        right--;
                    } else {
                        res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        left++;
                        right--;
                        while(left < right && nums[left] == nums[left-1]) ++left; // 跳过重复元素
                        while(left < right && nums[right] == nums[right+1]) --right; // 跳过重复元素
                    }
                } // end while
            }
        } // end outter for
        return res;
    }
}



解题误区及心得总结

误区
  • 对于利用有序序列进行剪枝的特性不熟悉
  • 对于有序序列元素间关系认识不够
  • 边界条件确定的不够细致深入
心得总结
  • 相比于三数之和target0容易剪枝,此处剪枝需要充分的利用数组是有序的特性.此处有以下两点:
    • 利用四数之和的最大值最小值与有序数组的特性进行剪枝
    • 利用有序数组的特性将剪枝条件进行优化.如从nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target优化为nums[i] + 3*nums[i+1] > target
  • 递归类型的子程序一般有先验条件,在调用子程序时应该满足子程序的先验条件.此处就体现在ij的边界条件上.
posted @ 2021-01-24 22:05  一生至为你  阅读(230)  评论(0)    收藏  举报