今天是周末,公司双休,本以为会有很多人留下来哈哈哈,结果我太天真了,我们这一层好像只来了两三个人。
我呢,由于有段时间没刷题了,就在公司刷题,今天给大家带来的是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的情况。
当然还是最普通最暴力的方法啦,四层循环遍历所有的情况,时间复杂度为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跟三数之和的是一样的,减少一层循环,使用三层循环遍历每种情况,同时利用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]);
还是跟三数之和一样,使用前后指针往中间夹的思路,只不过外加了一层循环而已。
中心思想:
不懂的话,看看三数之和讲解?吧,这里不再强调了,都是一样的思路。
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??!
想起一句话,细节决定成败!
正是因为一小部分的细节,使算法时间一下子提高了不少!很多场景都是这样的,比如火箭上的螺丝要是出了小小差错,估计整个火箭就有问题了!
无论做事或者写代码,最好都要考虑一下各种边界情况,因为往往是这些边界情况,对代码质量或产品质量起了决定性因素!