1. 问题描述
1.1 题目要求
给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a, b, c, d,使得 a + b + c + d = target?找出所有满足条件且不重复的四元组。
1.2 关键约束
- 答案中不可以包含重复的四元组
- 四元组的顺序不重要(
[a,b,c,d]和[b,a,c,d]视为相同) - 需要处理整数溢出问题(数组元素和可能超出int范围)
1.3 示例
java
示例1:
输入: nums = [1,0,-1,0,-2,2], target = 0
输出: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例2:
输入: nums = [2,2,2,2,2], target = 8
输出: [[2,2,2,2]]
示例3:
输入: nums = [1,-2,-5,-4,-3,3,3,5], target = -11
输出: [[-5,-4,-3,1]]
2. 算法思路
2.1 核心策略:排序 + 递归 + 双指针
整体框架采用分治思想,将复杂问题逐步简化:
- 排序预处理:对数组排序,便于后续去重和双指针查找
- 递归降维:将"n数之和"问题递归转化为"2数之和"问题
- 双指针求解:在2数之和中使用双指针法,O(n)时间复杂度求解
2.2 算法流程图
四数之和问题
↓
排序数组
↓
固定第一个数(遍历)
↓
固定第二个数(递归)
↓
双指针求解剩余两数
↓
收集结果 + 去重
3. 核心方法详解
3.1 主方法 fourSum()
java
static List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums); // 关键:先排序
List<List<Integer>> result = new LinkedList<>();
dfs(4, 0, nums.length - 1, target, nums, new LinkedList<>(), result);
return result;
}
作用:入口函数,负责排序和启动递归
3.2 递归方法 dfs() - n数之和通用解
3.2.1 方法签名
java
static void dfs(int n, int i, int j, long target, int[] nums,
LinkedList<Integer> stack, List<List<Integer>> result)
3.2.2 递归终止条件
java
if (n == 2) {
twoSum(i, j, nums, target, stack, result);
return;
}
当问题规模缩小到2数之和时,转为双指针法求解
3.2.3 递归过程与剪枝优化
java
for (int k = i; k < j - (n - 2); k++) {
// 去重逻辑
if (k > i && nums[k] == nums[k - 1]) continue;
stack.push(nums[k]); // 做选择
dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result); // 递归
stack.pop(); // 回溯
}
关键剪枝条件 :k < j - (n - 2)
原理:
- 需要确保当前位置
k后面还至少有n-1个元素可选 - 剩余元素个数 =
j - k(从k+1到j的闭区间) - 要求:
j - k >= n - 1→k <= j - (n - 1) - 由于循环是
k < ...(不包含边界),所以是j - (n - 2)
示例 :求4数之和,n=4,数组长度j=5
- 循环边界:
k < 5 - (4-2) = 3 - k最大为2,剩余索引3、4 → 刚好2个元素,满足需求
3.2.4 去重机制
java
if (k > i && nums[k] == nums[k - 1]) continue;
- 跳过同一层级的重复元素
k > i确保第一个元素不跳过(因为前面没有元素可比较)
3.3 双指针方法 twoSum()
3.3.1 方法签名
java
static void twoSum(int i, int j, int[] numbers, long target,
LinkedList<Integer> stack, List<List<Integer>> result)
3.3.2 核心逻辑
java
while (i < j) {
long sum = numbers[i] + numbers[j]; // 用long避免溢出
if (sum < target) i++;
else if (sum > target) j--;
else {
// 找到解,构建结果
ArrayList<Integer> list = new ArrayList<>(stack);
list.add(numbers[i]);
list.add(numbers[j]);
result.add(list);
i++;
j--;
// 跳过重复元素
while (i < j && numbers[i] == numbers[i - 1]) i++;
while (i < j && numbers[j] == numbers[j + 1]) j--;
}
}
3.3.3 去重细节
- 找到解后移动指针 :
i++和j-- - 跳过左侧重复 :
while (i < j && numbers[i] == numbers[i - 1]) i++; - 跳过右侧重复 :
while (i < j && numbers[j] == numbers[j + 1]) j--;
4. 复杂度分析
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n³) | 排序O(nlogn) + 外层循环O(n) × 内层递归O(n) × 双指针O(n) |
| 空间复杂度 | O(logn) | 递归栈空间(深度为n-2)+ 排序栈空间 + 结果存储 |
详细分析:
- 排序 :
Arrays.sort()使用快速排序,平均O(nlogn) - 递归:等效于三层嵌套循环(4数→3数→2数)
- 双指针:O(n)线性扫描
- 总体:主导项为O(n³),当n较大时排序开销可忽略
5. 关键技巧与细节
5.1 溢出处理
java
// 方法参数使用 long
dfs(..., long target, ...) { ... }
// 计算时也使用 long
long sum = numbers[i] + numbers[j];
target - (long) nums[k] // 强制类型转换
必要性 :int最大值约21亿,两个大数相加容易溢出
5.2 栈的使用
java
LinkedList<Integer> stack = new LinkedList<>();
stack.push(nums[k]); // 入栈
stack.pop(); // 出栈(回溯)
new ArrayList<>(stack) // 创建副本加入结果
优势:天然支持回溯,自动维护已选元素
5.3 结果存储顺序
由于递归前序遍历 和栈的LIFO特性 ,最终结果中每个四元组的顺序是从后往前的,但题目不关心顺序,符合要求。
6. 完整代码实现
java
package com.it.Y_Leetcode双指针;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
/**
* LeetCode 18 - 四数之和
*
* 算法思路:排序 + 递归 + 双指针
* 时间复杂度:O(n³)
* 空间复杂度:O(logn)
*/
public class SumLeetcode18 {
public static void main(String[] args) {
// 测试用例
System.out.println(fourSum(new int[]{1, 0, -1, 0, -2, 2}, 0));
System.out.println(fourSum(new int[]{2, 2, 2, 2, 2}, 8));
System.out.println(fourSum(new int[]{1, -2, -5, -4, -3, 3, 3, 5}, -11));
System.out.println(fourSum(new int[]{1000000000, 1000000000, 1000000000, 1000000000}, -294967296));
System.out.println(fourSum(new int[]{-1000000000, -1000000000, 1000000000, -1000000000, -1000000000}, 294967296));
}
/**
* 四数之和主方法
* @param nums 输入数组
* @param target 目标值
* @return 所有不重复的四元组
*/
static List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> result = new LinkedList<>();
dfs(4, 0, nums.length - 1, target, nums, new LinkedList<>(), result);
return result;
}
/**
* 递归求解 n 数之和
* @param n 数字个数
* @param i 起始索引
* @param j 结束索引
* @param target 目标值(long防溢出)
* @param nums 排序后的数组
* @param stack 已选数字栈
* @param result 结果集
*/
static void dfs(int n, int i, int j, long target, int[] nums,
LinkedList<Integer> stack, List<List<Integer>> result) {
if (n == 2) {
twoSum(i, j, nums, target, stack, result);
return;
}
// 剪枝:确保剩余元素足够
for (int k = i; k < j - (n - 2); k++) {
// 去重
if (k > i && nums[k] == nums[k - 1]) continue;
stack.push(nums[k]);
dfs(n - 1, k + 1, j, target - (long) nums[k], nums, stack, result);
stack.pop();
}
}
/**
* 双指针法求两数之和
* @param i 左指针
* @param j 右指针
* @param numbers 排序数组
* @param target 目标值
* @param stack 已选数字
* @param result 结果集
*/
static public void twoSum(int i, int j, int[] numbers, long target,
LinkedList<Integer> stack, List<List<Integer>> result) {
while (i < j) {
long sum = (long) numbers[i] + numbers[j];
if (sum < target) {
i++;
} else if (sum > target) {
j--;
} else {
// 找到解
ArrayList<Integer> list = new ArrayList<>(stack);
list.add(numbers[i]);
list.add(numbers[j]);
result.add(list);
i++;
j--;
// 去重
while (i < j && numbers[i] == numbers[i - 1]) i++;
while (i < j && numbers[j] == numbers[j + 1]) j--;
}
}
}
}
7. 测试用例分析
| 测试用例 | 数组 | target | 预期结果 | 测试目的 |
|---|---|---|---|---|
| 标准情况 | [1,0,-1,0,-2,2] | 0 | 3个四元组 | 基础功能 |
| 重复元素 | [2,2,2,2,2] | 8 | [[2,2,2,2]] | 去重能力 |
| 负数情况 | [1,-2,-5,-4,-3,3,3,5] | -11 | [[-5,-4,-3,1]] | 负数处理 |
| 大数溢出(正) | [1e9,1e9,1e9,1e9] | -294967296 | [] | 溢出防范 |
| 大数溢出(负) | [-1e9,-1e9,1e9,-1e9,-1e9] | 294967296 | [] | 溢出防范 |
8. 相关题目推荐
| 题目 | 难度 | 关联度 | 核心思路 |
|---|---|---|---|
| LeetCode 1 - 两数之和 | Easy | ★★★★☆ | 哈希表/双指针 |
| LeetCode 15 - 三数之和 | Medium | ★★★★★ | 排序+双指针(本题的简化版) |
| LeetCode 16 - 最接近的三数之和 | Medium | ★★★☆☆ | 排序+双指针 |
| LeetCode 454 - 四数相加 II | Medium | ★★★☆☆ | 哈希表分组 |
9. 总结
9.1 算法精髓
- 分治思想:将复杂问题层层分解,最终转化为简单问题
- 空间换时间:通过排序和额外空间,将暴力O(n⁴)优化到O(n³)
- 细节决定成败:溢出处理、去重逻辑、剪枝条件三个细节是AC关键
9.2 适用场景
本算法的递归框架 可推广到任意k数之和 问题,只需修改递归入口的n值即可。
9.3 常见错误
- ❌ 未处理
int溢出 → 使用long - ❌ 去重逻辑错误 → 注意
k > i和i < j条件 - ❌ 剪枝边界错误 → 牢记
j - (n - 2)