LeetCode刷题笔记-18.四数之和(4sum)
问题描述
给定一个包含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]
]
题解
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,j与j之后的数不可能满足四数之和大于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;
}
}
解题误区及心得总结
误区
- 对于利用有序序列进行剪枝的特性不熟悉
- 对于有序序列元素间关系认识不够
- 边界条件确定的不够细致深入
心得总结
- 相比于三数之和
target为0容易剪枝,此处剪枝需要充分的利用数组是有序的特性.此处有以下两点:- 利用四数之和的最大值和最小值与有序数组的特性进行剪枝
- 利用有序数组的特性将剪枝条件进行优化.如从
nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target优化为nums[i] + 3*nums[i+1] > target
- 递归类型的子程序一般有先验条件,在调用子程序时应该满足子程序的先验条件.此处就体现在
i和j的边界条件上.

浙公网安备 33010602011771号