18. 4Sum 四数之和:多语言实现、分析与拓展
一、题目分析
给定一个包含 n
个整数的数组 nums
和一个目标整数 target
,需要找出数组中所有满足 a + b + c + d = target
的唯一四元组 (a, b, c, d)
,且结果集中不能包含重复的四元组。
二、常用解法
排序 + 多指针法
- 思路:
- 排序:首先对数组
nums
进行排序,这是后续操作的基础。排序后数组元素按升序排列,方便通过指针移动来调整四元组的和。 - 多层遍历与双指针:使用两个指针进行外层遍历,分别设为
i
和j
,它们的移动范围决定了四元组中前两个数的取值。对于每一对(i, j)
,初始化另外两个指针k
和l
,k
指向j + 1
,l
指向数组末尾。通过移动k
和l
来找到满足和为target
的四元组。 - 和的比较与指针移动:计算当前四元组的和
sum = nums[i] + nums[j] + nums[k] + nums[l]
,将其与target
比较。若sum > target
,则将l
左移以减小和;若sum < target
,则将k
右移以增大和;若sum == target
,则找到一个满足条件的四元组,将其加入结果集,并对k
和l
进行去重移动,避免重复结果。 - 去重操作:在移动
i
、j
、k
和l
指针时,都要进行去重操作。例如,当sum == target
后,通过循环跳过k
和l
指向的重复元素;当外层指针i
和j
移动时,同样要跳过与前一个位置相同的元素,以确保结果集中不会出现重复的四元组。
- 排序:首先对数组
- 优点:这种方法通过排序和多指针的结合,有效地减少了需要遍历的组合数量,相较于暴力解法的 (O(n^4)) 时间复杂度,此方法将时间复杂度降低到 (O(n^3)),提高了算法效率。
三、多语言实现
Python实现
class Solution:
def fourSum(self, nums, target):
N = len(nums)
nums.sort()
res = []
i = 0
while i < N - 3:
j = i + 1
while j < N - 2:
k = j + 1
l = N - 1
remain = target - nums[i] - nums[j]
while k < l:
if nums[k] + nums[l] > remain:
l -= 1
elif nums[k] + nums[l] < remain:
k += 1
else:
res.append([nums[i], nums[j], nums[k], nums[l]])
while k < l and nums[k] == nums[k + 1]:
k += 1
while k < l and nums[l] == nums[l - 1]:
l -= 1
k += 1
l -= 1
while j < N - 2 and nums[j] == nums[j + 1]:
j += 1
j += 1
while i < N - 3 and nums[i] == nums[i + 1]:
i += 1
i += 1
return res
Java实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FourSum {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> res = new ArrayList<>();
if (nums == null || nums.length < 4) {
return res;
}
Arrays.sort(nums);
int n = nums.length;
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;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
List<Integer> quadruplet = new ArrayList<>();
quadruplet.add(nums[i]);
quadruplet.add(nums[j]);
quadruplet.add(nums[left]);
quadruplet.add(nums[right]);
res.add(quadruplet);
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return res;
}
}
C实现
#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于qsort排序
int compare(const void *a, const void *b) {
return (*(int *)a - *(int *)b);
}
// 辅助函数,用于判断四元组是否已存在
int exists(int **result, int size, int *quadruplet) {
for (int i = 0; i < size; i++) {
if (result[i][0] == quadruplet[0] && result[i][1] == quadruplet[1] && result[i][2] == quadruplet[2] && result[i][3] == quadruplet[3]) {
return 1;
}
}
return 0;
}
int** fourSum(int *nums, int numsSize, int target, int *returnSize, int **returnColumnSizes) {
if (numsSize < 4) {
*returnSize = 0;
return NULL;
}
qsort(nums, numsSize, sizeof(int), compare);
int **result = (int **)malloc(numsSize * numsSize * sizeof(int *));
*returnColumnSizes = (int *)malloc(numsSize * numsSize * sizeof(int));
*returnSize = 0;
for (int i = 0; i < numsSize - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < numsSize - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1;
int right = numsSize - 1;
while (left < right) {
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
int *quadruplet = (int *)malloc(4 * sizeof(int));
quadruplet[0] = nums[i];
quadruplet[1] = nums[j];
quadruplet[2] = nums[left];
quadruplet[3] = nums[right];
if (!exists(result, *returnSize, quadruplet)) {
result[*returnSize] = quadruplet;
(*returnColumnSizes)[*returnSize] = 4;
(*returnSize)++;
} else {
free(quadruplet);
}
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return result;
}
C++实现
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
if (nums.size() < 4) {
return res;
}
sort(nums.begin(), nums.end());
int n = nums.size();
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;
}
int left = j + 1;
int right = n - 1;
while (left < right) {
long long sum = (long long)nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
res.push_back({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--;
}
}
}
}
return res;
}
};
Go实现
package main
import (
"fmt"
"sort"
)
func fourSum(nums []int, target int) [][]int {
var res [][]int
if len(nums) < 4 {
return res
}
sort.Ints(nums)
for i := 0; i < len(nums)-3; i++ {
if i > 0 && nums[i] == nums[i - 1] {
continue
}
for j := i + 1; j < len(nums)-2; j++ {
if j > i + 1 && nums[j] == nums[j - 1] {
continue
}
left, right := j + 1, len(nums) - 1
for left < right {
sum := nums[i] + nums[j] + nums[left] + nums[right]
if sum > target {
right--
} else if sum < target {
left++
} else {
res = append(res, []int{nums[i], nums[j], nums[left], nums[right]})
for left < right && nums[left] == nums[left + 1] {
left++
}
for left < right && nums[right] == nums[right - 1] {
right--
}
left++
right--
}
}
}
}
return res
}
四、算法复杂性分析
时间复杂度
- 排序部分:对长度为
n
的数组进行排序,常见排序算法如快速排序、归并排序平均时间复杂度为 (O(n \log n))。 - 多层遍历与双指针部分:外层有两层循环,时间复杂度为 (O(n^2)),对于每一对外层循环的指针,内层双指针移动次数最多为 (n) 次,所以这部分时间复杂度为 (O(n))。总体时间复杂度为 (O(n^2 \times n) = O(n^3)),因为 (O(n^3)) 增长速度快于 (O(n \log n)),所以整个算法时间复杂度为 (O(n^3))。
空间复杂度
- 除输入数组外:使用的额外空间主要是存储结果集的空间,在最坏情况下,结果集可能包含 (O(n^3)) 个四元组(尽管实际中通常远小于这个数量)。此外,还使用了一些指针变量等常数级空间。如果不考虑输出空间,算法使用的额外空间为常数级,即 (O(1))。所以空间复杂度主要由结果集决定,在最坏情况下为 (O(n^3))。
五、实现的关键点和难度
关键点
- 排序:排序是整个算法的基础步骤,它使得数组有序,为后续通过指针移动来调整四元组的和提供了可能。通过排序,我们可以利用数组元素的顺序特性,快速判断和调整四元组的和与目标值的关系。
- 多层指针遍历与移动逻辑:合理设置多层指针的初始位置和移动条件是核心。外层两层指针遍历确定四元组中的前两个数,内层双指针遍历寻找满足和为
target
的后两个数。指针根据和与目标值的比较结果正确移动,保证能够遍历到所有可能的四元组组合。 - 去重操作:在指针移动过程中,对重复元素进行跳过操作,确保结果集中不会出现重复的四元组。这需要在每一层指针移动时都仔细处理,避免遗漏或错误地保留重复结果。
难度
- 多层循环嵌套与指针管理:多层循环嵌套增加了代码的复杂度,需要清晰地理解每一层循环的作用和边界条件。同时,多个指针的移动和管理需要谨慎处理,确保它们在正确的范围内移动,并且相互之间的配合能够正确遍历所有可能的组合。
- 去重逻辑的实现:去重逻辑在多层循环和指针移动的过程中实现,需要在不同的指针移动场景下都正确处理重复元素。例如,在找到一个满足条件的四元组后,不仅要对双指针指向的元素去重,还要对外层指针移动时的重复元素进行处理,这要求对算法的整体流程有深入的理解,以确保去重的完整性和正确性。
六、扩展及难度加深题目
扩展题目1:K数之和
给定一个数组 nums
和一个目标值 target
,找出数组中所有满足 (a_1 + a_2 + \cdots + a_k = target的唯一
k元组。可以将四数之和的方法扩展到一般的
k数之和问题,时间复杂度为 \(O(n^{k - 1})\),空间复杂度在最坏情况下为 \(O(n^{k - 1})\)。实现过程中需要处理多层循环和指针的移动以及去重逻辑,随着
k` 的增大,复杂度会显著增加。
扩展题目2:带权重的四数之和
给数组中的每个元素赋予一个权重,要求找出所有满足四数之和为 target
的四元组,并且使得四元组的权重之和最大(或最小)。这需要在寻找四元组的过程中,同时考虑权重因素,增加了算法的复杂性。不仅要关注数字之和,还要计算权重之和,并根据权重之和进行比较和筛选。
难度加深题目1:动态更新数组的四数之和
假设数组会动态更新(添加或删除元素),要求在每次更新后能够高效地更新满足四数之和为 target
的四元组集合。这可能需要使用更复杂的数据结构(如平衡二叉搜索树)来维护数组元素,以便在更新后快速重新计算结果。例如,使用红黑树来存储数组元素,在添加或删除元素后,通过调整树的结构来快速定位可能影响四数之和结果的元素范围,并重新计算。
难度加深题目2:多维数组的四数之和
给定一个多维数组(如二维数组,每个元素又是一个数组),要求在这些数组中找出所有满足四数之和为 target
的组合。这需要考虑如何遍历多维数组,并在不同维度上应用类似的算法思想。例如,可能需要先确定在多维数组中如何选择四个元素构成组合,然后通过排序和指针移动的方式来找到满足和为 target
的组合,实现起来较为复杂,需要处理好不同维度之间的关系和组合的唯一性。
七、应用场合
- 数据分析与挖掘:在数据分析中,可能需要从大量数据中找出满足特定数值关系的组合。例如,在金融数据分析中,寻找满足特定资金流动总和的四笔交易记录,通过四数之和问题的解法可以帮助发现潜在的交易模式或异常情况。
- 组合优化问题:在资源分配、任务调度等场景中,可能需要从多个资源或任务的参数中找出特定组合,使得它们的和满足某个条件。例如,在项目管理中,从多个任务的成本、时间等参数中找出四个任务,使得它们的总成本或总时间最接近目标值,四数之和问题的解法可以为这类组合优化问题提供思路和方法。
- 算法竞赛与面试:此类问题作为经典的算法问题,常用于算法竞赛和面试中,考察应聘者对数组操作、排序算法、多指针法以及去重等技术的掌握和应用能力。通过解决这类问题,可以检验应聘者的逻辑思维、代码实现能力以及对复杂问题的分析和解决能力。