深入解析Java中的四数之和问题:从基础到优化
在算法面试中,四数之和(4Sum)问题是非常经典的题目之一。它是“三数之和”(3Sum)问题的延伸,要求找到四个数字,使它们的和等于给定的目标值。本篇文章将带你详细探讨四数之和问题,从最基础的解法到优化后的高级解法,并提供具体的代码实现和优化策略。
问题描述
题目: 给定一个包含 n
个整数的数组 nums
和一个目标值 target
,找出数组中使得 a + b + c + d = target
的所有唯一四元组(a, b, c, d
)。注意:解集不能包含重复的四元组。
示例:
输入:nums = [1, 0, -1, 0, -2, 2], target = 0
输出:
[
[-1, 0, 0, 1],
[-2, -1, 1, 2],
[-2, 0, 0, 2]
]
基础解法:双指针法
最基本的解法是基于“双指针”的思想。具体步骤如下:
- 排序: 首先对数组进行排序,以便于后续跳过重复元素和使用双指针技巧。
- 固定两个数: 使用两层循环分别固定两个数,然后用双指针寻找剩下的两个数。
- 双指针查找: 通过两个指针分别从左右两端向中间移动,查找剩余两个数,使它们的和与目标值匹配。
代码实现:
import java.util.*;
public class Sum4 {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums); // 排序
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 跳过重复元素
for (int j = i + 1; j < nums.length - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 跳过重复元素
int left = j + 1, right = nums.length - 1;
while (left < right) {
long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left++; // 跳过重复元素
while (left < right && nums[right] == nums[right - 1]) right--; // 跳过重复元素
left++;
right--;
} else if (sum < target) {
left++; // 左指针右移
} else {
right--; // 右指针左移
}
}
}
}
return res;
}
}
解法分析
这个解法的时间复杂度为 O(n^3)
,其中 n
是数组的长度。虽然这种方法在一些情况下表现良好,但它并没有利用一些可能的优化技术来减少计算时间。
优化一:剪枝策略
在基础的双指针法中,我们可以利用“剪枝”技术来减少不必要的计算量,进一步提升效率。
剪枝策略:
- 提前终止循环: 如果当前元素和后面的最小三个元素之和已经大于目标值,那么后续的循环没有继续的必要,可以直接跳出循环。
- 提前跳过循环: 如果当前元素和数组中最大的三个元素之和仍然小于目标值,说明即使选择这些元素也无法达到目标值,因此可以跳过这些元素。
优化后的代码实现:
public List<List<Integer>> fourSum1(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;
for (int i = 0; i < n - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 剪枝:最小的四个数之和已经大于目标值
if ((long)nums[i] + nums[i + 1] + nums[i + 2] + nums[ i + 3] > target) break;
// 剪枝:当前数与最大的三个数之和仍小于目标值
if ((long)nums[i] + nums[n - 1] + nums[n - 2] + nums[n - 3] < target) continue;
for (int j = i + 1; j < n -2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
// 剪枝:最小的三个数与当前数之和已经大于目标值
if ((long)nums[i] + nums[j] + nums[j+1] + nums[j+2] > target) break;
// 剪枝:当前数与最大的两个数之和仍小于目标值
if ((long)nums[i] + nums[j] + nums[n-1] + nums[n-2] < target) continue;
int left = j+1, right = n-1;
while (left < right) {
long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left+1]) left++;
while (left < right && nums[right] == nums[right-1]) right--;
left++;
right--;
} else if (sum < target){
left++;
} else {
right--;
}
}
}
}
return res;
}
剪枝策略分析:
通过剪枝,许多不必要的循环被跳过或提前终止,这大大减少了算法的执行时间。特别是在数组非常大时,剪枝策略显得尤为重要。
优化二:利用哈希表加速查找
除了使用双指针法,我们还可以利用哈希表来减少查找的时间复杂度。具体来说,我们可以预先计算出数组中所有可能的两个元素的和,并将它们存储在哈希表中。这样,在查找另外两个元素时,我们只需要在哈希表中查找是否存在和为目标值的两个数。
实现步骤:
- 构建哈希表: 遍历数组中的每一对元素,并将它们的和存储在哈希表中,键为元素和,值为元素对的索引。
- 查找匹配: 再次遍历数组,查找是否存在另外两个元素的和与目标值匹配,若匹配则添加到结果集中。
代码实现:
public List<List<Integer>> fourSum2(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
Set<List<Integer>> resSet = new HashSet<>();
Map<Long, List<int[]>> twoSumMap = new HashMap<>();
int n = nums.length;
// 构建哈希表,存储所有两个元素的和及其索引
for (int i = 0; i < n - 1; i++) {
for (int j = i + 1; j < n; j++) {
long sum = (long)nums[i] + nums[j];
if (!twoSumMap.containsKey(sum)) {
twoSumMap.put(sum, new ArrayList<>());
}
twoSumMap.get(sum).add(new int[]{i, j});
}
}
// 查找匹配的四元组
for (int i = 0; i < n-3; i++) {
if (i > 0 && nums[i] == nums[i-1]) continue;
for (int j = i+1; j < n-2; j++) {
if (j > i+1 && nums[j] == nums[j-1]) continue;
long remain = (long) target - nums[i] - nums[j];
if (twoSumMap.containsKey(remain)) {
for (int[] pair : twoSumMap.get(remain)) {
int k = pair[0], l = pair[1];
if (k > j) {
List<Integer> resList = Arrays.asList(nums[i], nums[j], nums[k], nums[l]);
if (resSet.add(resList)) {
res.add(resList);
}
}
}
}
while (j < n - 2 && nums[j] == nums[j+1]) j++;
}
while (
i < n -3 && nums[i] == nums[i+1]) i++;
}
return res;
}
哈希表优化分析:
利用哈希表的解法在查找配对元素时效率更高,因为我们通过预先计算出的和直接查找匹配项,大大减少了搜索的时间。但这种方法在构建哈希表时需要额外的空间,因此在空间复杂度方面可能有所增加。
总结与比较
- 双指针法: 时间复杂度为
O(n^3)
,适用于小规模数组。在引入剪枝策略后,效率得到了一定的提升。 - 哈希表法: 虽然增加了空间复杂度,但在时间复杂度上有显著优化,适合需要快速查找的场景。
在实际应用中,可以根据问题规模和具体需求选择适合的方法。对于较小规模的数组,双指针法可能已经足够高效。而在需要处理大规模数据或追求更高效的查找时,哈希表法则是更好的选择。
希望通过这篇文章,你能更深入地理解四数之和问题的不同解法及其优化策略。如果你有任何疑问或更好的建议,欢迎在评论区留言,我们一起讨论和学习!