【LeetCode】详解四数之和18. 4Sum Given an array nums of n integers and an integer target, are there elements


前言

今天是周末,公司双休,本以为会有很多人留下来哈哈哈,结果我太天真了,我们这一层好像只来了两三个人。
我呢,由于有段时间没刷题了,就在公司刷题,今天给大家带来的是LeetCode第18题,四数之和,建议先看看我之前写的三数之和博客,因为思想都是一样的。
戳我跳转到三数之和讲解?


正文

原题

链接:四数之和

Given an array nums of n integers and an integer target, are there elements a, b, c, and d in nums such that a + b + c + d = target? Find all unique quadruplets in the array which gives the sum of target.
Note:
The solution set must not contain duplicate quadruplets.
Example:
Given array nums = [1, 0, -1, 0, -2, 2], and target = 0.
A solution set is:
[
[-1, 0, 0, 1],
[-2, -1, 1, 2],
[-2, 0, 0, 2]
]

题目大意
给定一个整型数组nums以及一个target值,找出所有四个数相加等于target的情况。

思路1

当然还是最普通最暴力的方法啦,四层循环遍历所有的情况,时间复杂度为O(N^4),代码也很简单。

代码

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        if (nums == null || nums.length < 4) return new LinkedList<>();
        
        // 思路1:使用四层循环
        Set<List<Integer>> result = new HashSet<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                for (int k = j + 1; k < nums.length; k++) {
                    for (int l = k + 1; l < nums.length; l++) {
                        if (nums[i] + nums[j] + nums[k] + nums[l] == target) {
                            result.add(Arrays.asList(nums[i], nums[j], nums[k], nums[l]));
                        }
                    }
                }
            }
        } 
        
        return new ArrayList(result);
    }
}

代码讲解

用了set数据结构,避免添加了重复的情况,然后就先使用Arrays.sort(nums);排序啦,排序的目的就是方便去重啦,三数之和的博客也讲到啦。
提交代码,时间跟空间都很慢,有时候会出现超时的情况!
在这里插入图片描述

思路2

思路2跟三数之和的是一样的,减少一层循环,使用三层循环遍历每种情况,同时利用set判断是否存在差值,这里的set存放的是一个数值噢,不像上一个思路一样存放List
比如第一层循环的数为nums[i],第二层循环的数为nums[j],第三层循环的数为nums[k],接着判断set中是否存在target与前三层之和nums[i] + nums[j] + nums[k]的差值target - nums[i] + nums[j] + nums[k],存在的话,即添加到list中。

代码

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        if (nums == null || nums.length < 4) return new LinkedList<>();
        
		// 思路2:使用三层循环,外加一个set,类似三数之和(二数之和)
        List<List<Integer>> result = new ArrayList<>();
        Set<Integer> set = new HashSet<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                for (int k = j + 1; k < nums.length; k++) {
                    if (set.contains(target - nums[i] - nums[j] - nums[k])) {
                        List<Integer> subList = new LinkedList<>();
                        subList.add(target - nums[i] - nums[j] - nums[k]);
                        subList.add(nums[i]);
                        subList.add(nums[j]);
                        subList.add(nums[k]);
                        // 判断是否存在,也就是为了去重
                        if (!result.contains(subList))
                            result.add(subList);
                    }
                }
            }
            set.add(nums[i]);
        }
        return result;
    }
}

代码讲解

这里跟三数之和一样,只不过是在三数之和的思路2中外加了一层循环,即可实现四数之和。

有一点需要提醒一下,跟三数之和一样,set必须先检测再添加,道理跟三数之和思路2一样的。

还是老样子,先排序数组,然后三层循环遍历,如果set存在差值,就先添加到子集合中;这时需要做一下去重判断if (!result.contains(subList)),只有result中不含有子集才进行添加。

第二三层循环遍历结束之后,再将第一层循环的值添加到set中,set.add(nums[i]);

提交代码,很显然比第一种快多了,但还是不够快!
在这里插入图片描述

思路3

还是跟三数之和一样,使用前后指针往中间夹的思路,只不过外加了一层循环而已。

中心思想:

  • 先排序!然后两层循环 + 前后指针移动(下面的sum为前两层循环的值加前后指针的值
  • sum大于target,则后指针往前移(使sum变小);
  • sum小于target,则前指针往后移(使sum变大)

不懂的话,看看三数之和讲解?吧,这里不再强调了,都是一样的思路。

代码

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        if (nums == null || nums.length < 4) return new LinkedList<>();
        
        // 思路3:使用左右指针往中间夹的思路
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) 
                continue;
            for (int j = i + 1; j < nums.length; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) 
                    continue;
                int start = j + 1;
                int end = nums.length - 1;
                int tempSum = nums[i] + nums[j];
                while (start < end) {
                    int sum = tempSum + nums[start] + nums[end];
                    if (sum == target) {
                        result.add(Arrays.asList(nums[i], nums[j], nums[start], nums[end]));
                        // 去重
                        while (start < end && nums[start] == nums[start + 1]) start++;
                        while (start < end && nums[end] == nums[end - 1]) end--;
                        start++;
                        end--;
                    } else if (sum < target) 
                        start++;
                    else 
                        end--;
                }
            }
        }
        return result;
    }
}

代码讲解

就连代码都是跟三数之和差不多的,不再废话,自己研究一下很容易看懂的。
只不过提交发现,时间竟然没有达到我的预期(比如超过90%+的人)!
在这里插入图片描述
带着疑惑,我去看了一下讨论区,发现最快的是3ms,我经常差了17ms,惭愧啊!!!

不过看了一下对方的解释,果然3ms的思路就是niubility??!先放张图吧,图中就是对方的解释:
在这里插入图片描述
好吧,思路其实也是前后指针,只不过对方加了一些优化,也就是红圈中的啦,结合对方的代码,就很容易看懂了!
在这里插入图片描述
红圈就是优化的内容,也就是我的代码多的内容,我来解释一下!

有一种情况就是,如果第一层循环的数nums[i] 与 最大值也就是nums[nums.length - 1]的三倍 之和还小于target的话,那直接跳过,进行第二次循环!

什么意思呢?

如果最大值的三倍跟第一层循环的数之和小于目标值的话,那后面的操作也就可以不用继续了,直接continue;把第一层循环的数变大!应该很容易理解吧!下面的代码就是用来解决这种情况的:

if( nums[i] + 3 * nums[nums.length - 1] < target ) // current num is too small
	continue;

另外一种情况就是,当前数太大了,想一想,如果第一层循环的数(也就是最小值)的四倍比target还大的话,那后面肯定不用再算了,因为最小值的四倍都比target大,后面的数之和肯定越来越大!直接退出循环break;即可!

下面的代码就是用来解决这种情况的:

if( nums[i] * 4 > target ) // current num is too large
    break;

上面两段代码就是第一个红圈的内容,作者也说得很明白了;

第二个红圈也是一样的,判断了数值太大或者太小的情况,正是因为这几行代码,一下子优化了不少!

添加优化之和,我的代码变成了这样:

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        if (nums == null || nums.length < 4) return new LinkedList<>();
        
        // 思路3:使用左右指针往中间夹的思路
        List<List<Integer>> result = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) 
                continue;
            if (nums[i] + 3 * nums[nums.length - 1] < target ) // 当前数太小了,无法凑到target,可以直接跳过
                continue;
            if (nums[i] * 4 > target ) // 当前数太大了,这时候由于当前数是数组中的最小值,后面也就没有必要继续探测
                break;
            for (int j = i + 1; j < nums.length; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) 
                    continue;
                if (nums[i] + nums[j] + 2 * nums[nums.length - 1] < target ) // 当前数太小了,无法凑到target,可以直接跳过
                    continue;
                if (nums[i] + nums[j] * 3 > target ) // 当前数太大了,这时候由于当前数是数组中的最小值,后面也就没有必要继续探测
                    break;
                int start = j + 1;
                int end = nums.length - 1;
                int tempSum = nums[i] + nums[j];
                while (start < end) {
                    int sum = tempSum + nums[start] + nums[end];
                    if (sum == target) {
                        result.add(Arrays.asList(nums[i], nums[j], nums[start], nums[end]));
                        // 去重
                        while (start < end && nums[start] == nums[start + 1]) start++;
                        while (start < end && nums[end] == nums[end - 1]) end--;
                        start++;
                        end--;
                    } else if (sum < target) 
                        start++;
                    else 
                        end--;
                }
            }
        }
        return result;
    }
}

是的变得更长了,但是提交之后你会发现!时间一下子优化了不少!至少看起来就很舒服!niubility??!
在这里插入图片描述


总结

想起一句话,细节决定成败!

正是因为一小部分的细节,使算法时间一下子提高了不少!很多场景都是这样的,比如火箭上的螺丝要是出了小小差错,估计整个火箭就有问题了!

无论做事或者写代码,最好都要考虑一下各种边界情况,因为往往是这些边界情况,对代码质量或产品质量起了决定性因素!

发布了57 篇原创文章 · 获赞 282 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/weixin_41463193/article/details/100050330