目录
一、问题描述
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复 的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
二、解题思路
这道题目是经典的 "四数之和" 问题,它的本质是在一个整数数组中,寻找四个不同的数,使它们的和等于给定的目标值。题目还要求返回所有可能的、不重复的四元组。
为了有效解决这个问题,我们可以采用基于 排序+双指针 的思想,类似于解决 "两数之和" 和 "三数之和" 的方法,但需要引入更多的边界条件和去重逻辑,确保四元组的唯一性。下面是具体的解题思路:
解题步骤:
-
数组排序: 首先对数组进行升序排序,便于后续使用双指针法查找数对。同时,排序后的数组有利于去重处理,也能通过提前终止条件优化算法效率。
-
固定前两个数 : 通过两层循环分别确定四个数中的前两个数
nums[i]
和nums[j]
。此时,问题就简化成 "两数之和" 问题,即从剩余的数中找到两个数nums[left]
和nums[right]
使它们的和等于target - nums[i] - nums[j]
。 -
双指针寻找后两个数 : 对于确定的前两个数
nums[i]
和nums[j]
,我们在剩下的数中通过双指针法,设定左右两个指针left
和right
,从nums[j+1]
到nums[length-1]
之间寻找满足条件的两个数。- 如果当前和等于目标值,则将其加入结果集。
- 如果当前和小于目标值,则左指针右移,增大和。
- 如果当前和大于目标值,则右指针左移,减小和。
-
去重处理: 为了避免重复的四元组,需要对数组中的重复元素进行过滤。对于每一层循环中的当前数值,如果和前一个数相等,则跳过该数,避免处理相同的组合。
-
提前终止条件: 由于数组已经排序,我们可以通过提前判断组合中最小值和最大值是否可能满足目标和,来减少不必要的计算:
- 如果当前最小的四个数的和已经大于
target
,后续更大的数肯定无法满足条件,直接跳出循环。 - 如果当前最大的四个数的和还小于
target
,则跳过这次循环,继续尝试更大的数。
- 如果当前最小的四个数的和已经大于
三、代码
java
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> quadruplets = new ArrayList<>();
// 如果数组长度小于4,直接返回空
if (nums == null || nums.length < 4) {
return quadruplets;
}
// 排序数组,方便去重和双指针处理
Arrays.sort(nums);
int length = nums.length;
// 第一层循环,枚举第一个数
for (int i = 0; i < length - 3; i++) {
// 避免重复的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 提前结束的优化条件1:当前组合的最小值已经大于target
if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
break;
}
// 提前跳过的优化条件2:当前组合的最大值还小于target
if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
// 第二层循环,枚举第二个数
for (int j = i + 1; j < length - 2; j++) {
// 避免重复的第二个数
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
// 提前结束的优化条件1:当前组合的最小值已经大于target
if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
break;
}
// 提前跳过的优化条件2:当前组合的最大值还小于target
if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
continue;
}
// 双指针寻找剩余两个数
int left = j + 1, right = length - 1;
while (left < right) {
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
quadruplets.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 跳过重复的第三个数
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
left++;
// 跳过重复的第四个数
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
right--;
} else if (sum < target) {
left++; // 当前和小于target,左指针右移
} else {
right--; // 当前和大于target,右指针左移
}
}
}
}
return quadruplets;
}
}
四、复杂度分析
时间复杂度:
-
排序的时间复杂度 :
O(n log n)
,因为我们需要先对数组进行排序。 -
双重循环的时间复杂度 :
- 第一层循环
i
需要遍历n - 3
次。 - 第二层循环
j
需要遍历n - i - 2
次。 - 内部的双指针部分在每次固定
i
和j
后,最多需要遍历O(n)
次,进行一次线性扫描。
因此,双重循环的时间复杂度为
O(n^3)
,综合排序时间,整个算法的时间复杂度为O(n^3)
。 - 第一层循环
空间复杂度:
- 额外空间使用 :在排序之后,除了存储结果的
quadruplets
列表,算法几乎没有使用额外的空间。 - 空间复杂度 :
O(n)
,主要用于存储结果四元组的列表。