目录
- 1.问题描述
- 2.问题分析
- 3.算法设计与实现
-
- [3.1 排序+双指针法(最优解)](#3.1 排序+双指针法(最优解))
- [3.2 哈希表法](#3.2 哈希表法)
- [3.3 暴力枚举法](#3.3 暴力枚举法)
- [3.4 分治法](#3.4 分治法)
- [3.5 双指针优化法](#3.5 双指针优化法)
- 4.性能分析对比
-
- [4.1 复杂度对比](#4.1 复杂度对比)
- [4.2 性能分析](#4.2 性能分析)
- [4.3 选择建议](#4.3 选择建议)
- 5.边界情况处理
-
- [5.1 常见边界情况](#5.1 常见边界情况)
- [5.2 边界测试用例](#5.2 边界测试用例)
- [5.3 边界处理技巧](#5.3 边界处理技巧)
- 6.扩展与变体
-
- [6.1 四数之和](#6.1 四数之和)
- [6.2 最接近的三数之和](#6.2 最接近的三数之和)
- [6.3 三数之和小于目标值](#6.3 三数之和小于目标值)
- [6.4 三数之和的多重集合](#6.4 三数之和的多重集合)
- 7.总结
-
- [7.1 核心知识点总结](#7.1 核心知识点总结)
- [7.2 算法思维提升](#7.2 算法思维提升)
- [7.3 实际应用场景](#7.3 实际应用场景)
1.问题描述
给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k,同时还满足 nums[i] + nums[j] + nums[k] == 0。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例 1
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0
不同的三元组是 [-1,0,1] 和 [-1,-1,2]
注意,输出的顺序和三元组的顺序并不重要
示例 2
输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0
示例 3
输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0
约束条件
3 <= nums.length <= 3000-10^5 <= nums[i] <= 10^5
2.问题分析
理解题目
本题要求在整数数组中找出所有不重复的三元组,使得三个数之和为0。需要理解的关键点:
- 三元组定义:三个不同的数组元素(下标不同),但元素值可以相同
- 和为0条件:三个数的和必须等于0
- 不重复要求:不能有重复的三元组,即使元素顺序不同也算重复
- 顺序无关:返回的三元组顺序不重要,但三元组内元素的顺序需要固定(通常是升序)
关键挑战
- 时间复杂度:数组长度最大3000,需要设计高效算法,不能使用O(n³)的暴力解法
- 去重复杂度:需要避免返回重复的三元组,去重逻辑需要精心设计
- 负数处理:数组包含负数,需要考虑正负数组合
- 空间限制:虽然题目没有明确空间限制,但需要考虑算法空间复杂度
核心洞察
- 排序的威力:将数组排序后,可以更高效地寻找三元组,并方便去重
- 转换为两数之和:固定一个数后,问题转化为在剩余数组中寻找两数之和等于目标值
- 双指针技巧:对于排序后的数组,可以使用双指针在O(n)时间内找到两数之和
- 去重策略:通过跳过相同的元素,可以有效避免重复三元组
- 提前终止:当固定的数大于0时,由于数组已排序,后面的数都大于0,三数之和不可能为0
破题关键
- 排序预处理:首先对数组排序,这是后续所有优化策略的基础
- 固定+双指针:外层循环固定一个数,内层使用双指针寻找另外两个数
- 去重机制 :
- 外层循环:如果当前数与前一个数相同,跳过
- 内层双指针:找到解后,跳过所有相同的左指针和右指针
- 提前剪枝 :
- 如果固定的数大于0,直接结束(因为数组已升序排序)
- 如果固定的数与前一个数相同,跳过以避免重复
3.算法设计与实现
3.1 排序+双指针法(最优解)
核心思想
将数组排序后,固定一个数,将问题转化为在剩余数组中寻找两数之和等于目标值(0减去固定的数),使用双指针法在O(n)时间内解决。
算法思路
- 排序预处理:将数组按升序排序
- 外层循环 :遍历数组,固定第一个数
nums[i]- 如果
nums[i] > 0,直接结束(因为数组已排序,后面的数都大于0) - 如果
i > 0且nums[i] == nums[i-1],跳过当前循环(去重)
- 如果
- 内层双指针 :设置左指针
left = i+1,右指针right = n-1- 计算三数之和
sum = nums[i] + nums[left] + nums[right] - 如果
sum == 0,找到解,加入结果集,并移动左右指针跳过重复元素 - 如果
sum < 0,左指针右移(增加和) - 如果
sum > 0,右指针左移(减少和)
- 计算三数之和
- 返回结果:遍历完成后返回所有找到的三元组
时间复杂度分析
- 排序:O(n log n)
- 外层循环:O(n)
- 内层双指针:每个外层循环中O(n)
- 总时间复杂度:O(n²)
空间复杂度分析
- 排序所需空间:O(log n)(快速排序递归栈)
- 结果存储空间:O(k),k为结果数量
- 额外空间:O(1)
代码实现
java
import java.util.*;
public class ThreeSumTwoPointers {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
// 边界条件检查
if (nums == null || nums.length < 3) {
return result;
}
// 步骤1:排序
Arrays.sort(nums);
int n = nums.length;
// 步骤2:遍历数组,固定第一个数
for (int i = 0; i < n - 2; i++) {
// 剪枝1:如果第一个数大于0,后面的数都大于0,三数之和不可能为0
if (nums[i] > 0) {
break;
}
// 去重1:跳过相同的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 步骤3:使用双指针寻找另外两个数
int left = i + 1;
int right = n - 1;
int target = -nums[i]; // 需要找的两个数之和应为-nums[i]
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
// 找到解
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重2:跳过相同的左指针元素
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// 去重3:跳过相同的右指针元素
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
// 移动指针继续寻找
left++;
right--;
} else if (sum < target) {
// 和太小,左指针右移
left++;
} else {
// 和太大,右指针左移
right--;
}
}
}
return result;
}
}
3.2 哈希表法
核心思想
固定一个数后,使用哈希表存储已遍历的元素,将问题转化为在剩余数组中寻找两数之和等于目标值。
算法思路
- 排序预处理:将数组按升序排序(便于去重)
- 外层循环 :遍历数组,固定第一个数
nums[i]- 如果
nums[i] > 0,直接结束 - 如果
i > 0且nums[i] == nums[i-1],跳过当前循环
- 如果
- 内层哈希表 :使用HashSet存储已遍历的元素
- 遍历
j从i+1到n-1 - 计算需要的补数
complement = -nums[i] - nums[j] - 如果HashSet中包含补数,找到解,加入结果集
- 将当前元素加入HashSet
- 遍历
- 去重处理 :找到解后,跳过所有相同的
nums[j] - 返回结果:遍历完成后返回所有找到的三元组
时间复杂度分析
- 排序:O(n log n)
- 外层循环:O(n)
- 内层循环:每个外层循环中O(n)
- 哈希表操作:O(1)
- 总时间复杂度:O(n²)
空间复杂度分析
- 哈希表存储:O(n)
- 排序所需空间:O(log n)
- 结果存储空间:O(k)
- 总空间复杂度:O(n)
代码实现
java
import java.util.*;
public class ThreeSumHashMap {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 3) {
return result;
}
// 排序便于去重
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
// 剪枝:第一个数大于0,不可能有三数之和为0
if (nums[i] > 0) {
break;
}
// 去重:跳过相同的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 使用HashSet存储已遍历的元素
Set<Integer> seen = new HashSet<>();
int target = -nums[i];
for (int j = i + 1; j < n; j++) {
int complement = target - nums[j];
if (seen.contains(complement)) {
// 找到解
result.add(Arrays.asList(nums[i], complement, nums[j]));
// 去重:跳过相同的nums[j]
while (j + 1 < n && nums[j] == nums[j + 1]) {
j++;
}
}
// 将当前元素加入HashSet
seen.add(nums[j]);
}
}
return result;
}
}
3.3 暴力枚举法
核心思想
使用三重循环枚举所有可能的三元组,检查其和是否为0,同时进行去重。这种方法虽然简单直观,但时间复杂度高,不适用于大规模数据。
算法思路
- 三重循环:使用三层嵌套循环枚举所有三元组
- 检查条件:对于每个三元组,检查下标是否不同且和为0
- 去重处理:使用HashSet或排序后比较来避免重复三元组
- 返回结果:收集所有满足条件的三元组
时间复杂度分析
- 三重循环:O(n³)
- 去重操作:O(1)(使用HashSet)
- 总时间复杂度:O(n³)
空间复杂度分析
- 去重使用的HashSet:O(k),k为结果数量
- 总空间复杂度:O(k)
代码实现
java
import java.util.*;
public class ThreeSumBruteForce {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 3) {
return result;
}
int n = nums.length;
// 使用Set去重
Set<String> seen = new HashSet<>();
// 三重循环枚举所有三元组
for (int i = 0; i < n - 2; i++) {
for (int j = i + 1; j < n - 1; j++) {
for (int k = j + 1; k < n; k++) {
// 检查三数之和是否为0
if (nums[i] + nums[j] + nums[k] == 0) {
// 创建三元组并排序,生成唯一标识
List<Integer> triplet = Arrays.asList(nums[i], nums[j], nums[k]);
Collections.sort(triplet);
String key = triplet.toString();
// 检查是否已存在
if (!seen.contains(key)) {
result.add(triplet);
seen.add(key);
}
}
}
}
}
return result;
}
}
3.4 分治法
核心思想
将数组分成两部分,分别处理,然后合并结果。这种方法更适用于学术研究,实际应用中不如双指针法高效。
算法思路
- 排序预处理:将数组按升序排序
- 分治递归 :
- 将数组分成左右两部分
- 递归处理左右部分的三数之和问题
- 处理跨越左右两部分的三元组
- 合并结果:合并左右两部分的结果,去重
- 返回结果:返回最终的三元组列表
时间复杂度分析
- 排序:O(n log n)
- 分治递归:O(n² log n)
- 合并操作:O(n²)
- 总时间复杂度:O(n² log n)
空间复杂度分析
- 递归栈空间:O(log n)
- 结果存储空间:O(k)
- 总空间复杂度:O(n)
代码实现
java
import java.util.*;
public class ThreeSumDivideConquer {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 3) {
return result;
}
// 排序
Arrays.sort(nums);
// 使用分治法
return threeSumHelper(nums, 0, nums.length - 1);
}
private List<List<Integer>> threeSumHelper(int[] nums, int left, int right) {
List<List<Integer>> result = new ArrayList<>();
// 递归终止条件:区间太小
if (right - left < 2) {
return result;
}
// 分治:将区间分成两部分
int mid = left + (right - left) / 2;
// 递归处理左右两部分
List<List<Integer>> leftResult = threeSumHelper(nums, left, mid);
List<List<Integer>> rightResult = threeSumHelper(nums, mid + 1, right);
// 合并结果
result.addAll(leftResult);
result.addAll(rightResult);
// 处理跨越左右两部分的三元组
for (int i = left; i <= mid; i++) {
// 跳过重复元素
if (i > left && nums[i] == nums[i - 1]) {
continue;
}
// 在右半部分使用双指针寻找另外两个数
int target = -nums[i];
int l = mid + 1;
int r = right;
while (l < r) {
int sum = nums[l] + nums[r];
if (sum == target) {
result.add(Arrays.asList(nums[i], nums[l], nums[r]));
// 跳过重复元素
while (l < r && nums[l] == nums[l + 1]) l++;
while (l < r && nums[r] == nums[r - 1]) r--;
l++;
r--;
} else if (sum < target) {
l++;
} else {
r--;
}
}
}
// 去重
return removeDuplicates(result);
}
private List<List<Integer>> removeDuplicates(List<List<Integer>> list) {
Set<String> seen = new HashSet<>();
List<List<Integer>> result = new ArrayList<>();
for (List<Integer> triplet : list) {
String key = triplet.toString();
if (!seen.contains(key)) {
result.add(triplet);
seen.add(key);
}
}
return result;
}
}
3.5 双指针优化法
核心思想
在标准双指针法的基础上,进一步优化去重逻辑和剪枝策略,减少不必要的比较和操作。
算法思路
- 排序预处理:将数组按升序排序
- 外层循环优化 :
- 增加更多剪枝条件
- 优化去重逻辑
- 内层双指针优化 :
- 提前计算目标值
- 优化指针移动策略
- 返回结果:遍历完成后返回所有找到的三元组
时间复杂度分析
- 排序:O(n log n)
- 外层循环:O(n)
- 内层双指针:每个外层循环中O(n)
- 总时间复杂度:O(n²)
空间复杂度分析
- 排序所需空间:O(log n)
- 结果存储空间:O(k)
- 额外空间:O(1)
代码实现
java
import java.util.*;
public class ThreeSumOptimized {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 3) {
return result;
}
// 排序
Arrays.sort(nums);
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
// 优化剪枝1:如果最小的数都大于0,不可能有三数之和为0
if (nums[i] > 0) {
break;
}
// 优化剪枝2:如果最大的三个数都小于0,也不可能有三数之和为0
if (nums[i] + nums[n-2] + nums[n-1] < 0) {
continue;
}
// 去重:跳过相同的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 提前计算目标值
int target = -nums[i];
int left = i + 1;
int right = n - 1;
while (left < right) {
// 优化:提前计算两数之和
int twoSum = nums[left] + nums[right];
if (twoSum == target) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 优化去重:同时移动左右指针跳过重复元素
int currentLeft = nums[left];
int currentRight = nums[right];
while (left < right && nums[left] == currentLeft) {
left++;
}
while (left < right && nums[right] == currentRight) {
right--;
}
} else if (twoSum < target) {
// 优化:跳过重复的左指针元素
int currentLeft = nums[left];
while (left < right && nums[left] == currentLeft) {
left++;
}
} else {
// 优化:跳过重复的右指针元素
int currentRight = nums[right];
while (left < right && nums[right] == currentRight) {
right--;
}
}
}
}
return result;
}
}
4.性能分析对比
4.1 复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 实现难度 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 排序+双指针法 | O(n²) | O(log n) | ★★☆☆☆ | 性能最优,代码简洁 | 需要排序,改变原始顺序 |
| 哈希表法 | O(n²) | O(n) | ★★★☆☆ | 思路直观,易于理解 | 空间占用大,去重复杂 |
| 暴力枚举法 | O(n³) | O(k) | ★☆☆☆☆ | 实现简单,逻辑清晰 | 时间复杂度高,不实用 |
| 分治法 | O(n² log n) | O(n) | ★★★★☆ | 学术价值高,体现分治思想 | 实现复杂,性能一般 |
| 双指针优化法 | O(n²) | O(log n) | ★★★☆☆ | 优化去重和剪枝,性能好 | 代码稍复杂 |
4.2 性能分析
-
时间复杂度对比:
- 双指针法和哈希表法都是O(n²),但双指针法常数因子更小
- 暴力法O(n³)在n=3000时完全不可行
- 分治法O(n² log n)理论上比双指针法稍差
-
空间复杂度对比:
- 双指针法和分治法主要使用递归栈空间
- 哈希表法需要额外O(n)空间存储哈希表
- 暴力法需要存储结果去重的空间
-
实际性能:
- 在LeetCode测试中,双指针法通常是最快的
- 哈希表法由于哈希冲突和去重逻辑,性能略差
- 优化版双指针法在特定情况下有微小优势
4.3 选择建议
- 面试场景:首选排序+双指针法,展示算法思维
- 竞赛场景:双指针法或优化版双指针法
- 生产环境:根据数据特点选择,通常双指针法最优
- 学习场景:建议理解所有方法,掌握不同算法思想
5.边界情况处理
5.1 常见边界情况
- 数组长度不足3:直接返回空列表
- 全正数或全负数数组:不可能有三数之和为0
- 全零数组 :返回
[[0,0,0]] - 包含重复元素:需要正确处理去重
- 数组元素值边界 :元素值在-105到105之间,需要注意溢出
5.2 边界测试用例
java
// 测试各种边界情况
int[][] testCases = {
{}, // 空数组
{1, 2}, // 长度不足3
{1, 2, 3}, // 全正数
{-1, -2, -3}, // 全负数
{0, 0, 0}, // 全零
{0, 0, 0, 0}, // 多个零
{-1, 0, 1, 2, -1, -4}, // 示例用例
{0, 0, 0, 1, -1}, // 多个零与正负数混合
{1, 1, -2}, // 重复元素
{-2, 0, 1, 1, 2}, // 多种组合
};
5.3 边界处理技巧
- 数组长度检查 :方法开头检查
nums == null || nums.length < 3 - 排序后剪枝 :
- 如果第一个元素大于0,直接返回空列表
- 如果最后一个元素小于0,直接返回空列表
- 去重策略 :
- 外层循环:跳过相同的固定元素
- 内层双指针:找到解后跳过相同的左右指针元素
- 整数溢出:本题中元素范围有限,求和不会溢出,但作为通用解法应考虑
6.扩展与变体
6.1 四数之和
在数组中找出所有和为target的四元组。
java
public class FourSum {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
if (nums == null || nums.length < 4) return result;
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;
long twoSumTarget = (long)target - nums[i] - nums[j];
while (left < right) {
long sum = (long)nums[left] + nums[right];
if (sum == twoSumTarget) {
result.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 < twoSumTarget) {
left++;
} else {
right--;
}
}
}
}
return result;
}
}
6.2 最接近的三数之和
找到数组中三个数的和最接近target。
java
public class ThreeSumClosest {
public int threeSumClosest(int[] nums, int target) {
if (nums == null || nums.length < 3) return 0;
Arrays.sort(nums);
int n = nums.length;
int closestSum = nums[0] + nums[1] + nums[2];
int minDiff = Math.abs(closestSum - target);
for (int i = 0; i < n - 2; i++) {
// 去重
if (i > 0 && nums[i] == nums[i - 1]) continue;
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
int diff = Math.abs(sum - target);
// 更新最接近的和
if (diff < minDiff) {
minDiff = diff;
closestSum = sum;
}
if (sum == target) {
return target; // 正好等于目标值
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return closestSum;
}
}
6.3 三数之和小于目标值
统计数组中三个数的和小于target的组合数量。
java
public class ThreeSumSmaller {
public int threeSumSmaller(int[] nums, int target) {
if (nums == null || nums.length < 3) return 0;
Arrays.sort(nums);
int n = nums.length;
int count = 0;
for (int i = 0; i < n - 2; i++) {
int left = i + 1;
int right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum < target) {
// 如果sum < target,那么对于固定的i和left,right取left+1到right之间的任何值都满足
count += right - left;
left++;
} else {
right--;
}
}
}
return count;
}
}
6.4 三数之和的多重集合
允许使用同一个元素多次(如果数组中有重复元素)。
java
public class ThreeSumMulti {
public int threeSumMulti(int[] nums, int target) {
long[] count = new long[101]; // 题目限定值范围
for (int num : nums) {
count[num]++;
}
long result = 0;
// 遍历所有可能的组合
for (int i = 0; i <= 100; i++) {
for (int j = i; j <= 100; j++) {
int k = target - i - j;
if (k < 0 || k > 100) continue;
if (i == j && j == k) {
// 三个数相同:C(n, 3)
result += count[i] * (count[i] - 1) * (count[i] - 2) / 6;
} else if (i == j && j != k) {
// 两个数相同,一个不同:C(n, 2) * count[k]
result += count[i] * (count[i] - 1) / 2 * count[k];
} else if (i < j && j < k) {
// 三个数都不同
result += count[i] * count[j] * count[k];
}
}
}
return (int)(result % 1_000_000_007);
}
}
7.总结
7.1 核心知识点总结
- 排序预处理:排序是解决三数之和问题的关键第一步
- 双指针技巧:在排序数组中使用双指针高效寻找两数之和
- 去重策略:通过跳过相同元素避免重复三元组
- 剪枝优化:提前终止不可能的搜索路径
- 算法思维:将三数问题转化为两数问题
7.2 算法思维提升
- 问题转化能力:将复杂问题转化为已知问题
- 双指针运用:掌握在排序数组中寻找两数之和的技巧
- 去重策略设计:设计高效的去重机制
- 剪枝优化思维:提前排除不可能的情况,提高算法效率
7.3 实际应用场景
- 数据分析:在数据集中寻找特定的数值组合
- 金融分析:寻找投资组合的最优配置
- 密码学:寻找满足特定条件的数值组合
- 游戏开发:计算游戏中的数值组合
面试建议
- 首选解法:排序+双指针法,思路清晰,代码简洁
- 解题步骤 :
- 分析问题,明确要求
- 提出排序预处理的想法
- 解释双指针法的原理
- 详细说明去重策略
- 分析时间复杂度和空间复杂度
- 代码质量:注重代码的可读性和健壮性
- 沟通能力:清晰解释算法思路和优化策略
三数之和问题是算法面试中的经典问题,它综合考察了排序、双指针、去重等多个重要算法技巧。掌握这个问题不仅有助于通过面试,还能提升解决复杂问题的能力。