题目描述
题目链接
https://leetcode.cn/problems/4sum/
思路
最朴素的方法是使用四重循环枚举所有的四元组,然后使用哈希表进行去重操作,得到不包含重复四元组的最终答案。假设数组的长度是,则该方法中,枚举的时间复杂度为
,去重操作的时间复杂度和空间复杂度也很高,因此我们需要换一种思路。
为了避免枚举到重复四元组,则需要保证每一重循环枚举到的元素不小于其上一重循环枚举到的元素,且在同一重循环中不能多次枚举到相同的元素。为了实现上述要求,可以对数组进行排序,并在循环过程中遵循以下两点:
- 每一种循环枚举到的下标必须大于上一重循环枚举到的下标;
- 同一重循环中,如果当前元素与上一个元素相同,则跳过当前元素。
使用上述方法,可以避免枚举到重复四元组,但是由于仍使用四重循环,时间复杂度仍是。然而它是很容易继续优化的,注意到数组已经被排序,因此可以使用双指针的方法去掉一重循环。使用两重循环分别枚举前两个数,然后在两重循环枚举到的数之后使用双指针枚举剩下的两个数。假设两重循环枚举到的前两个数分别位于下标
和
,其中
。初始时,左右指针分别指向下标
和下标
。每次计算四个数的和,并进行如下操作:
- 如果和等于
,则将枚举到的四个数加到答案中,然后将左指针右移直到遇到不同的数,将右指针左移直到遇到不同的数;
- 如果和小于
,则将左指针右移一位;
- 如果和大于
,则将右指针左移一位。
使用双指针枚举剩下的两个数的时间复杂度是,因此总时间复杂度是
,低于
。
具体实现时,还可以进行一些剪枝操作:
- 在确定第一个数之后,如果
,说明此时剩下的三个数无论取什么值,四数之和一定大于
,因此退出第一重循环;
- 在确定第一个数之后,如果
,说明此时剩下的三个数无论取什么值,四数之和一定小于
,因此第一重循环直接进入下一轮,枚举
;
- 在确定前两个数之后,如果
,说明此时剩下的两个数无论取什么值,四数之和一定大于
,因此退出第二重循环;
在确定前两个数之后,如果
,说明此时剩下的两个数无论取什么值,四数之和一定小于
,因此第二重循环直接进入下一轮,枚举
。
代码实现
public List<List<Integer>> fourSum(int[] nums, int target) {if (nums == null || nums.length < 4) {return new ArrayList<>();}int length = nums.length;List<List<Integer>> res = new ArrayList<>();// 排序Arrays.sort(nums);// 枚举第一个数for (int first = 0; first < length - 3; first++) {// 跳过重复数字if (first > 0 && nums[first] == nums[first - 1]) {continue;}// 提前剪枝if ((long) nums[first] + nums[first + 1] + nums[first + 2] + nums[first + 3] > target) {break;}if ((long) nums[first] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {continue;}// 枚举第二个数for (int second = first + 1; second < length - 2; second++) {// 跳过重复数字if (second > first + 1 && nums[second] == nums[second - 1]) {continue;}// 提前剪枝if ((long) nums[first] + nums[second] + nums[second + 1] + nums[second + 2] > target) {break;}if ((long) nums[first] + nums[second] + nums[length - 2] + nums[length - 1] < target) {continue;}// 第三、四个数采用滑动窗口思想int third = second + 1, fourth = length - 1;while (third < fourth) {int sum = nums[first] + nums[second] + nums[third] + nums[fourth];// 窗口右侧左滑if (sum > target) {// 滑动时要跳过重复数字int next = fourth - 1;while (next > third && nums[next] == nums[fourth]) {next--;}fourth = next;} else {// 符合条件的四元组if (sum == target) {List<Integer> tmp = new ArrayList<>();tmp.add(nums[first]);tmp.add(nums[second]);tmp.add(nums[third]);tmp.add(nums[fourth]);res.add(tmp);}// 窗口左侧右滑,滑动时要跳过重复数字int next = third + 1;while (next < fourth && nums[next] == nums[third]) {next++;}third = next;}}}}return res;}
复杂度分析
时间复杂度:
,其中
是数组的长度。排序的时间复杂度是
,枚举四元组的时间复杂度是
,因此总时间复杂度为
。
- 空间复杂度:
,其中
是数组的长度。空间复杂度主要取决于排序额外使用的空间。此外排序修改了输入数组
,实际情况中不一定允许,因此也可以看成使用了一个额外的数组存储了数组
的副本并排序,空间复杂度为
。
