Files
interview/16-LeetCode Hot 100/三数之和.md

314 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 三数之和 (3Sum)
LeetCode 15. Medium
## 题目描述
给你一个整数数组 `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
```
## 解题思路
### 核心思想
**排序 + 双指针**:先排序,固定第一个数,再用双指针找后两个数。
### 算法流程
1. **排序数组**:便于去重和双指针操作
2. **遍历第一个数**
- 跳过重复元素
- 如果当前数 > 0直接退出后面都 > 0
3. **双指针找后两个数**
- left = i + 1, right = len(nums) - 1
- 根据 sum 与 0 的关系移动指针
- 跳过重复元素
### 复杂度分析
- **时间复杂度**O(n²),排序 O(n log n) + 双指针 O(n²)
- **空间复杂度**O(1),不考虑结果存储
---
## Go 解法
```go
func threeSum(nums []int) [][]int {
result := [][]int{}
n := len(nums)
// 排序
sort.Ints(nums)
for i := 0; i < n-2; i++ {
// 去重:跳过重复的第一个数
if i > 0 && nums[i] == nums[i-1] {
continue
}
// 优化:如果最小数 > 0后面不可能有解
if nums[i] > 0 {
break
}
// 双指针
left, right := i+1, n-1
for left < right {
sum := nums[i] + nums[left] + nums[right]
if sum == 0 {
result = append(result, []int{nums[i], nums[left], nums[right]})
// 去重:跳过重复的 left
for left < right && nums[left] == nums[left+1] {
left++
}
// 去重:跳过重复的 right
for left < right && nums[right] == nums[right-1] {
right--
}
// 移动指针
left++
right--
} else if sum < 0 {
// 和太小left 向右移
left++
} else {
// 和太大right 向左移
right--
}
}
}
return result
}
```
### Go 代码要点
1. `sort.Ints()` 排序
2. `append()` 添加结果
3. 多重去重逻辑
---
## Java 解法
```java
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length - 2; i++) {
// 去重:跳过重复的第一个数
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
// 优化:如果最小数 > 0后面不可能有解
if (nums[i] > 0) {
break;
}
// 双指针
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重:跳过重复的 left
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
// 去重:跳过重复的 right
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
}
```
### Java 代码要点
1. `Arrays.sort()` 排序
2. `Arrays.asList()` 创建列表
3. `ArrayList` 存储结果
---
## 图解过程
```
数组: [-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
第一轮: i = 0, nums[i] = -4
left = 1, right = 5
sum = -4 + (-1) + 2 = -3 < 0
left++
left = 2, right = 5
sum = -4 + (-1) + 2 = -3 < 0
left++
left = 3, right = 5
sum = -4 + 0 + 2 = -2 < 0
left++
left = 4, right = 5
sum = -4 + 1 + 2 = -1 < 0
left++
left >= right, 退出
第二轮: i = 1, nums[i] = -1
left = 2, right = 5
sum = -1 + (-1) + 2 = 0 ✓
结果: [-1, -1, 2]
left = 3, right = 4
sum = -1 + 0 + 1 = 0 ✓
结果: [-1, 0, 1]
第三轮: i = 2, nums[i] = -1 (重复,跳过)
第四轮: i = 3, nums[i] = 0 > 0, 退出
最终结果: [[-1,-1,2], [-1,0,1]]
```
---
## 进阶问题
### Q1: 如果是四数之和?
**方法**:在三层循环 + 双指针,时间 O(n³)
```go
func fourSum(nums []int, target int) [][]int {
result := [][]int{}
sort.Ints(nums)
n := len(nums)
for i := 0; i < n-3; i++ {
if i > 0 && nums[i] == nums[i-1] {
continue
}
for j := i + 1; j < n-2; j++ {
if j > i+1 && nums[j] == nums[j-1] {
continue
}
left, right := j+1, n-1
for left < right {
sum := nums[i] + nums[j] + nums[left] + nums[right]
if sum == target {
result = append(result, []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--
} else if sum < target {
left++
} else {
right--
}
}
}
}
return result
}
```
### Q2: 如果数组很大,如何优化?
**优化**
1. 提前终止:`nums[i] * 3 > target`(正数情况)
2. 二分查找:确定第二个数后,二分查找后两个
3. 哈希表:空间换时间
---
## P7 加分项
### 深度理解
- **排序的作用**:去重 + 双指针基础
- **双指针原理**:利用有序性,单向移动
- **去重策略**:多处去重,确保结果唯一
### 实战扩展
- **大数据场景**:外部排序 + 分段处理
- **分布式场景**MapReduce 框架
- **业务场景**:推荐系统、用户画像匹配
### 变形题目
1. [16. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/)
2. [18. 四数之和](https://leetcode.cn/problems/4sum/)
3. [259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/)
---
## 总结
这道题的核心是:
1. **排序**:为双指针和去重创造条件
2. **固定一个数**:将问题转化为两数之和
3. **双指针**:根据 sum 与 target 的关系移动指针
4. **多重去重**i、left、right 都要跳过重复元素
**易错点**
- 忘记排序
- 去重逻辑不完整
- left 和 right 的移动条件
- 优化提前终止的条件
**最优解法**:排序 + 双指针,时间 O(n²),空间 O(1)