Files
interview/16-LeetCode Hot 100/三数之和-改进版示例.md

731 lines
16 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]]
```
**示例 2**:
```
输入:nums = [0,1,1]
输出:[]
```
## 思路推导
### 暴力解法分析
```python
def threeSum_brute(nums):
result = []
n = len(nums)
for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
if nums[i] + nums[j] + nums[k] == 0:
# 使用集合去重
triplet = sorted([nums[i], nums[j], nums[k]])
if triplet not in result:
result.append(triplet)
return result
```
**时间复杂度**: O(n³)
**空间复杂度**: O(1) (不考虑结果存储)
**问题**: n=2000时,操作次数约80亿次,超时!
### 优化思考 - 降维思想
**观察**: 固定第一个数后,问题变成"两数之和"
```
三数之和: nums[i] + nums[j] + nums[k] = 0
↓ 固定 nums[i]
两数之和: nums[j] + nums[k] = -nums[i]
```
**两数之和的优化**:
- 暴力: O(n²) 遍历所有对
- 优化: O(n) 双指针(前提:数组有序)
**总复杂度**: O(n) × O(n) = O(n²) ✅
### 为什么排序后能用双指针?
**核心原理:单调性**
```
有序数组: [-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
如果 sum = nums[left] + nums[right] < 0:
- 需要增大和
- left++ → nums[left]增大(数组升序) → sum增大
- right-- → nums[right]减小 → sum减小 ✗
如果 sum = nums[left] + nums[right] > 0:
- 需要减小和
- right-- → nums[right]减小 → sum减小
- left++ → nums[left]增大 → sum增大 ✗
```
**为什么无序数组不行?**
```
无序: [2, -1, 0, -4, 1]
↑ ↑ ↑
i L R
sum = 2 + (-1) + 1 = 2 > 0
应该移动哪个指针? 无法确定!
```
### 排序的三大作用
1. **去重基础**:相同数字相邻,便于跳过
2. **双指针前提**:利用单调性优化
3. **提前终止**:排序后,如果当前数>0,后面都>0
## 解题思路
### 核心思想
**排序 + 双指针 + 去重**
- 排序:为双指针和去重创造条件
- 固定一个数:将三数问题降维为两数问题
- 双指针: O(n) 解决两数之和
- 多重去重:避免重复结果
### 算法流程(详细版)
#### 步骤1:预处理 - 排序
```python
nums.sort() # [-1,0,1,2,-1,-4] → [-4,-1,-1,0,1,2]
```
**为什么排序?**
```
原始: [-1,0,1,2,-1,-4] → 重复:-1出现两次
排序: [-4,-1,-1,0,1,2] → 重复:-1相邻,便于跳过
```
#### 步骤2:外层循环 - 固定第一个数
```python
for i in range(len(nums) - 2): # ← 为什么-2?留2个数给双指针
# 去重1:跳过重复的第一个数
if i > 0 and nums[i] == nums[i-1]:
continue
# 提前终止:如果最小数>0,后面不可能和为0
if nums[i] > 0:
break
# 双指针找后两个数
left, right = i + 1, len(nums) - 1
...
```
**关键点解析**:
**Q1: 为什么循环到 `len(nums)-2`?**
```
数组: [0, 1, 2]
索引: 0 1 2
如果 i = 2 (最后一个元素):
left = 3 → 越界!
所以 i 最大 = len(nums) - 3 = 1
循环条件: range(len(nums) - 2) → [0, 1]
```
**Q2: 为什么判断 `i > 0`?**
```
i = 0:第一个元素,没有前一个元素,不用判断重复
i > 0:后续元素,需要判断是否与前一个相同
错误写法:
if nums[i] == nums[i-1]: # i=0时越界!
正确写法:
if i > 0 and nums[i] == nums[i-1]: # 安全
```
**Q3: 为什么break而不是continue?**
```
排序后: [-4, -1, -1, 0, 1, 2]
i=3, nums[i]=0 > 0
后续: [1, 2] 都 > 0
三数和: 0 + 1 + 2 = 3 > 0
结论:后面不可能有和为0的组合,直接退出
```
#### 步骤3:内层双指针 - 两数之和
```python
while left < right:
current_sum = nums[i] + nums[left] + nums[right]
if current_sum == 0:
result.append([nums[i], nums[left], nums[right]])
# 去重2:跳过重复的left
while left < right and nums[left] == nums[left+1]:
left += 1
# 去重3:跳过重复的right
while left < right and nums[right] == nums[right-1]:
right -= 1
# 继续寻找下一组
left += 1
right -= 1
elif current_sum < 0:
left += 1 # 需要更大的和
else:
right -= 1 # 需要更小的和
```
**为什么找到答案后要同时移动两个指针?**
```
数组: [-2, 0, 1, 1, 2]
i L R
找到: -2 + 0 + 2 = 0 ✓
如果只移动L: L=2, nums[L]=1
sum = -2 + 1 + 2 = 1 > 0 → R--
但这样会错过可能的组合
正确:同时移动
L=1, R=3: -2 + 1 + 1 = 0 ✓ (找到第二个)
```
### 关键细节说明
#### 细节1:去重逻辑的三重保障
```python
# 去重1:外层循环,跳过重复的第一个数
if i > 0 and nums[i] == nums[i-1]:
continue
# 去重2:找到答案后,跳过重复的left
while left < right and nums[left] == nums[left+1]:
left += 1
# 去重3:找到答案后,跳过重复的right
while left < right and nums[right] == nums[right-1]:
right -= 1
```
**示例**:
```
输入: [-2, -1, -1, 0, 1, 1, 2]
第一轮: i=0, nums[i]=-2
双指针找到: [-2, 0, 2], [-2, 1, 1]
第二轮: i=1, nums[i]=-1
双指针找到: [-1, -1, 2], [-1, 0, 1]
第三轮: i=2, nums[i]=-1
与i=1相同 → 跳过 (去重1)
第四轮: i=3, nums[i]=0
双指针找到: [0, -1, 1] → 已存在
第五轮: i=4, nums[i]=1
与i=3相同 → 跳过 (去重1)
结果: [[-2,0,2], [-2,1,1], [-1,-1,2], [-1,0,1]]
```
#### 细节2:为什么 `left < right` 而不是 `<=`?
```python
while left < right: # 正确
...
while left <= right: # 错误
...
```
**原因**:
```
left = right 时,只有一个元素
两数之和需要两个不同的元素
所以 left < right,不允许相同位置
```
#### 细节3:为什么先去重再移动?
```python
# 正确顺序
while left < right and nums[left] == nums[left+1]:
left += 1 # 先跳过所有重复
left += 1 # 再移动到新元素
# 错误顺序
left += 1 # 先移动
while left < right and nums[left] == nums[left-1]:
left += 1 # 可能漏掉某些重复
```
### 边界条件分析
#### 边界1:数组长度不足
```
输入: [0, 1]
输出: []
分析: len(nums) = 2
循环: range(2-2) = range(0) → 不执行
```
#### 边界2:全部为0
```
输入: [0, 0, 0, 0]
输出: [[0, 0, 0]]
排序: [0, 0, 0, 0]
i=0:
left=1, right=3
sum = 0+0+0 = 0 ✓ → result = [[0,0,0]]
去重left: left=1,2,3 (跳过所有0)
去重right: right=3,2,1
left >= right,退出内层循环
i=1:
nums[1] == nums[0] → 跳过
最终: [[0, 0, 0]]
```
#### 边界3:没有答案
```
输入: [0, 1, 2]
输出: []
排序: [0, 1, 2]
i=0:
left=1, right=2
sum = 0+1+2 = 3 > 0 → right--
left=1, right=1 → left >= right,退出
i=1:
nums[1] = 1 > 0 → break
最终: []
```
### 复杂度分析(详细版)
#### 时间复杂度
```
1. 排序: O(n log n)
- 快速排序平均情况
2. 外层循环: O(n)
for i in range(n)
3. 内层双指针: O(n)
while left < right
- 每次循环 left++ 或 right--
- 最多执行 n 次
4. 总复杂度: O(n log n) + O(n²) = O(n²)
- n² >> n log n (当 n 较大时)
- 渐近复杂度取最高阶
```
#### 空间复杂度
```
1. 排序栈空间: O(log n)
- 快速排序递归深度
2. 结果存储: O(k)
- k 为结果数量
3. 指针变量: O(1)
4. 总复杂度: O(log n) (不考虑结果存储)
```
## 代码实现
```python
def threeSum(nums):
"""
三数之和 - 排序 + 双指针解法
Args:
nums: 输入数组
Returns:
所有不重复的三元组,和为0
"""
result = []
nums.sort() # 步骤1:排序
# 步骤2:外层循环,固定第一个数
for i in range(len(nums) - 2):
# 去重1:跳过重复的第一个数
if i > 0 and nums[i] == nums[i-1]:
continue
# 提前终止:如果最小数>0,后面不可能和为0
if nums[i] > 0:
break
# 步骤3:双指针找后两个数
left, right = i + 1, len(nums) - 1
while left < right:
current_sum = nums[i] + nums[left] + nums[right]
if current_sum == 0:
# 找到答案
result.append([nums[i], nums[left], nums[right]])
# 去重2:跳过重复的left
while left < right and nums[left] == nums[left+1]:
left += 1
# 去重3:跳过重复的right
while left < right and nums[right] == nums[right-1]:
right -= 1
# 继续寻找下一组
left += 1
right -= 1
elif current_sum < 0:
# 和太小,需要增大 → left++
left += 1
else:
# 和太大,需要减小 → right--
right -= 1
return result
```
## 执行过程演示
### 输入: nums = [-1, 0, 1, 2, -1, -4]
```
初始状态:
nums = [-1, 0, 1, 2, -1, -4]
排序后: [-4, -1, -1, 0, 1, 2]
result = []
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一轮: i = 0, nums[i] = -4
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -4 + (-1) + 2 = -3 < 0
left++ → left = 1
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -4 + (-1) + 2 = -3 < 0
left++ → left = 2
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -4 + (-1) + 2 = -3 < 0
left++ → left = 3
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -4 + 0 + 2 = -2 < 0
left++ → left = 4
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -4 + 1 + 2 = -1 < 0
left++ → left = 5
left >= right,退出内层循环
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第二轮: i = 1, nums[i] = -1
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -1 + (-1) + 2 = 0 ✓
result = [[-1, -1, 2]]
去重left: nums[2] = -1 == nums[3] = 0? No
去重right: nums[5] = 2 == nums[4] = 1? No
left++, right-- → left = 3, right = 4
[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
sum = -1 + 0 + 1 = 0 ✓
result = [[-1, -1, 2], [-1, 0, 1]]
去重left: nums[3] = 0 == nums[4] = 1? No
去重right: nums[4] = 1 == nums[3] = 0? No
left++, right-- → left = 4, right = 3
left >= right,退出内层循环
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第三轮: i = 2, nums[i] = -1
[-4, -1, -1, 0, 1, 2]
i
判断: nums[2] == nums[1] == -1? Yes
跳过 (去重1)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第四轮: i = 3, nums[i] = 0
[-4, -1, -1, 0, 1, 2]
i
判断: nums[3] > 0? Yes
break (提前终止)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
最终结果: [[-1, -1, 2], [-1, 0, 1]]
```
## 常见错误
### 错误1:忘记排序
❌ 错误写法:
```python
def threeSum(nums):
result = []
for i in range(len(nums)):
left, right = i + 1, len(nums) - 1
while left < right:
if nums[i] + nums[left] + nums[right] == 0:
result.append([nums[i], nums[left], nums[right]])
# ... 双指针移动
return result
```
**问题**:无序数组无法使用双指针
✅ 正确写法:
```python
nums.sort() # 先排序!
for i in range(len(nums) - 2):
...
```
### 错误2:去重逻辑不完整
❌ 错误写法:
```python
if nums[i] == nums[i-1]: # i=0时越界
continue
```
✅ 正确写法:
```python
if i > 0 and nums[i] == nums[i-1]:
continue
```
### 错误3:指针移动条件错误
❌ 错误写法:
```python
if current_sum == 0:
result.append([nums[i], nums[left], nums[right]])
left += 1 # 只移动一个指针
```
✅ 正确写法:
```python
if current_sum == 0:
result.append([nums[i], nums[left], nums[right]])
# 去重
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
# 同时移动
left += 1
right -= 1
```
### 错误4:循环边界错误
❌ 错误写法:
```python
for i in range(len(nums)): # 可能越界
left, right = i + 1, len(nums) - 1
```
✅ 正确写法:
```python
for i in range(len(nums) - 2): # 留2个位置
left, right = i + 1, len(nums) - 1
```
## 变体问题
### 变体1:四数之和 (LeetCode 18)
**题目**:找出四数之和等于target的所有组合
**思路**:三层循环 + 双指针
```python
def fourSum(nums, target):
result = []
nums.sort()
for i in range(len(nums) - 3):
for j in range(i + 1, len(nums) - 2):
left, right = j + 1, len(nums) - 1
while left < right:
sum = nums[i] + nums[j] + nums[left] + nums[right]
if sum == target:
result.append([nums[i], nums[j], nums[left], nums[right]])
# 去重...
elif sum < target:
left += 1
else:
right -= 1
return result
```
**时间复杂度**: O(n³)
### 变体2:最接近的三数之和 (LeetCode 16)
**题目**:找出三数之和最接近target的组合
**思路**:双指针 + 记录最小差值
```python
def threeSumClosest(nums, target):
nums.sort()
closest = float('inf')
for i in range(len(nums) - 2):
left, right = i + 1, len(nums) - 1
while left < right:
current_sum = nums[i] + nums[left] + nums[right]
if abs(current_sum - target) < abs(closest - target):
closest = current_sum
if current_sum == target:
return target # 完全匹配
elif current_sum < target:
left += 1
else:
right -= 1
return closest
```
## 总结
### 核心要点
1. **排序的作用**
- 去重基础(相同元素相邻)
- 双指针前提(利用单调性)
- 提前终止(最小数>target时退出)
2. **降维思想**
- 三数之和 → 固定一个数 → 两数之和
- O(n³) → O(n²)
3. **去重策略**
- 外层循环:跳过重复的第一个数
- 内层循环:找到答案后跳过重复的left和right
- 多重去重确保结果唯一
4. **双指针原理**
- 利用有序数组的单调性
- 根据sum与target的关系单向移动指针
- 时间复杂度从O(n²)降到O(n)
### 易错点
- [ ] 忘记排序
- [ ] 去重逻辑不完整(i>0判断)
- [ ] 循环边界错误(len(nums)-2)
- [ ] 找到答案后只移动一个指针
- [ ] 提前终止条件用continue而非break
### 最优解法
**排序 + 双指针**
- 时间复杂度: O(n²)
- 空间复杂度: O(log n) (排序栈空间)
### P7加分项
**深度理解**:
- 排序的三大作用(去重、双指针、提前终止)
- 双指针的原理(单调性)
- 降维思想(三维→二维)
**实战扩展**:
- 大数据场景:外部排序 + 分段处理
- 分布式场景:MapReduce框架
- 业务场景:推荐系统、用户画像匹配
**变体题目**:
- [16. 最接近的三数之和](https://leetcode.cn/problems/3sum-closest/)
- [18. 四数之和](https://leetcode.cn/problems/4sum/)
- [259. 较小的三数之和](https://leetcode.cn/problems/3sum-smaller/)