Files
interview/16-LeetCode Hot 100/三数之和.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

725 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]]
解释:
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
```
## 思路推导
### 暴力解法分析
**最直观的思路**:三层循环枚举所有可能的三元组。
```python
def threeSum(nums):
result = set()
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]])
result.add(tuple(triplet))
return [list(t) for t in result]
```
**时间复杂度**O(n³)
- 外层循环O(n)
- 中层循环O(n)
- 内层循环O(n)
- 总计O(n³)
**空间复杂度**O(1),不考虑结果存储
**问题分析**
1. 效率太低n=2000 时n³ = 8×10⁹ 次运算,会超时
2. 去重困难:需要额外的集合操作
3. 无法利用已知信息优化
### 优化思考 - 第一步:降维
**观察**:固定第一个数后,问题变成"两数之和"
```python
# 固定第一个数 nums[i]
# 问题转化为:在 nums[i+1:] 中找两个数,使和为 -nums[i]
```
**为什么这样思考?**
- 三数之和 = 固定一个数 + 两数之和
- 两数之和可以用双指针 O(n) 解决
- 总复杂度O(n) × O(n) = O(n²)
**优化后的思路**
```python
for i in range(n):
target = -nums[i]
# 在 nums[i+1:] 中用双指针找两数之和为 target
twoSum(nums, i+1, target)
```
### 优化思考 - 第二步:双指针的前提条件
**问题**:为什么需要排序?
**关键理解**:双指针依赖数组的**单调性**
```
假设数组有序:[-4, -1, -1, 0, 1, 2]
↑ ↑ ↑
i left right
如果 nums[left] + nums[right] < target:
- 由于数组升序,增大 left → 和会变大
- 减小 right → 和会变小
- 所以应该 left++
如果 nums[left] + nums[right] > target:
- 减小 right → 和会变小
- 所以应该 right--
```
**不排序的后果**
- 无法确定指针移动方向
- 可能遗漏正确答案
- 无法利用有序性进行剪枝
### 优化思考 - 第三步:去重策略
**去重的三个关键点**
1. **外层去重**:跳过重复的第一个数
```python
if i > 0 and nums[i] == nums[i-1]:
continue
```
2. **内层去重**:找到答案后跳过重复元素
```python
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
```
3. **为什么排序有利于去重**
- 相同的数会相邻
- 只需比较相邻元素即可去重
- 时间复杂度从 O(n²) 降到 O(n)
## 解题思路
### 核心思想
**排序 + 双指针**:先排序,固定第一个数,再用双指针找后两个数。
**为什么这样思考?**
1. **排序的作用**
- 去除重复结果(相同数相邻)
- 使数组有序,才能使用双指针
- 提前终止(如果当前数>0后面都>0
2. **双指针的原理**
- 数组有序后,如果 sum < target需要增大 → left++
- 如果 sum > target需要减小 → right--
- 利用单调性,避免暴力枚举
3. **降维思想**
- 三数之和 → 固定一个数 → 两数之和
- O(n³) → O(n²)
### 详细算法流程
**步骤1预处理 - 排序**
```python
nums.sort() # O(n log n)
```
**作用**
- 去重:相同元素相邻
- 双指针基础:利用有序性
- 提前终止:最小数>0则退出
**步骤2外层循环 - 固定第一个数**
```python
for i in range(len(nums) - 2):
# 去重:跳过重复元素
if i > 0 and nums[i] == nums[i-1]:
continue
# 提前终止:如果最小数>0后面不可能=0
if nums[i] > 0:
break
# 双指针找后两个数
left, right = i + 1, len(nums) - 1
target = -nums[i]
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
result.append([nums[i], nums[left], nums[right]])
# 去重跳过重复的left
while left < right and nums[left] == nums[left+1]:
left += 1
# 去重跳过重复的right
while left < right and nums[right] == nums[right-1]:
right -= 1
# 同时移动,寻找下一组解
left += 1
right -= 1
elif current_sum < target:
left += 1 # 需要更大的和
else:
right -= 1 # 需要更小的和
```
**关键点详解**
1. **为什么循环到 `len(nums)-2`**
- 需要留2个数给双指针
- i 最大只能到 n-3
2. **为什么判断 `i > 0`**
- 第一个元素不用判断重复
- 避免越界访问
3. **为什么用 `break` 而不是 `continue`**
- 后面都>0不可能和为0
- 直接退出外层循环
**步骤3内层双指针 - 两数之和**
```python
def twoSum(nums, start, target):
left, right = start, len(nums) - 1
while left < right:
current_sum = nums[left] + nums[right]
if current_sum == target:
result.append([-target, nums[left], nums[right]])
# 去重跳过重复的left
while left < right and nums[left] == nums[left+1]:
left += 1
# 去重跳过重复的right
while left < right and nums[right] == nums[right-1]:
right -= 1
# 同时移动,寻找下一组解
left += 1
right -= 1
elif current_sum < target:
left += 1 # 需要更大的和
else:
right -= 1 # 需要更小的和
```
**关键点**
- 为什么找到答案后还要跳过重复?避免重复结果
- 为什么找到答案后要同时移动?继续寻找其他组合
### 关键细节说明
**细节1为什么是 `if i > 0`**
```python
# 错误写法
if nums[i] == nums[i-1]: # i=0时会越界
continue
# 正确写法
if i > 0 and nums[i] == nums[i-1]: # 第一个元素不用判断
continue
```
**细节2为什么找到答案后要同时移动**
```
假设:[-2, 0, 1, 1, 2]
i L R
找到:-2 + 0 + 2 = 0 ✓
如果只移动一个指针:
- L++: [-2, 0, 1, 1, 2] → -2 + 1 + 2 = 1 > 0 → R--
但这样可能错过其他组合
正确做法:同时移动
- L++ and R--: 继续寻找其他可能的组合
```
**细节3为什么break而不是continue**
```python
if nums[i] > 0:
break # 正确:后面的数都>0不可能和为0
# continue # 错误:会继续无意义的循环
```
**推理**
- 数组已排序
- 如果 nums[i] > 0则 nums[i+1] >= nums[i] > 0
- 任意三个正数相加不可能为0
- 直接退出,节省时间
**细节4为什么使用 `while` 而不是 `if` 去重?**
```python
# 错误写法:只跳过一个重复元素
if nums[left] == nums[left+1]:
left += 1
# 正确写法:跳过所有重复元素
while left < right and nums[left] == nums[left+1]:
left += 1
```
**示例**
```
数组:[-2, -1, -1, -1, 0, 1, 2]
i L R
找到:-2 + (-1) + 3 = 0 ✓
如果用 if
只跳过一个-1还会重复
如果用 while
跳过所有-1避免重复
```
### 边界条件分析
**边界1数组长度不足**
```
输入:[0, 1]
输出:[]
原因:长度<3无法组成三元组
处理:循环条件 range(len(nums)-2) 自动处理
```
**边界2全部为0**
```
输入:[0, 0, 0, 0]
输出:[[0, 0, 0]]
去重逻辑:只保留一个组合
过程:
- i=0: 找到 [0,0,0],跳过后续重复
- i=1: nums[1]==nums[0],跳过
- i=2: nums[2]==nums[1],跳过
```
**边界3有重复元素**
```
输入:[-1, -1, 0, 1]
输出:[[-1, 0, 1]]
去重逻辑:跳过第二个-1
过程:
- i=0: 找到 [-1,0,1]
- i=1: nums[1]==nums[0],跳过
```
**边界4最小的正整数情况**
```
输入:[-2, -1, 0, 1, 2]
输出:[[-2, 0, 2], [-2, -1, 3], [-1, 0, 1]]
提前终止i=0时nums[i]=-2<0继续
i=3时nums[i]=1>0退出
```
### 复杂度分析(详细版)
**时间复杂度**
```
- 排序O(n log n)
- 外层循环O(n)
- 内层双指针O(n)
- 总计O(n log n) + O(n²) = O(n²)
为什么主项是O(n²)
- n² >> n log n (当n较大时)
- 渐近复杂度取最高阶
```
**空间复杂度**
```
- 排序O(log n) (快速排序栈空间)
- 结果存储O(k) (k为结果数量)
- 指针变量O(1)
- 总计O(log n) (不考虑结果存储)
```
---
## 图解过程
```
数组: [-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]]
```
---
## 代码实现
```go
func threeSum(nums []int) [][]int {
result := [][]int{}
n := len(nums)
// 步骤1排序
sort.Ints(nums)
// 步骤2外层循环固定第一个数
for i := 0; i < n-2; i++ {
// 去重:跳过重复的第一个数
if i > 0 && nums[i] == nums[i-1] {
continue
}
// 提前终止:如果最小数>0后面不可能=0
if nums[i] > 0 {
break
}
// 双指针找后两个数
left, right := i+1, n-1
target := -nums[i]
for left < right {
sum := nums[left] + nums[right]
if sum == target {
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 < target {
left++ // 需要更大的和
} else {
right-- // 需要更小的和
}
}
}
return result
}
```
---
## 执行过程演示
**输入**[-1, 0, 1, 2, -1, -4]
**排序后**[-4, -1, -1, 0, 1, 2]
```
i=0, nums[i]=-4, target=4
left=1, right=5: -1+2=1 < 4 → left++
left=2, right=5: -1+2=1 < 4 → left++
left=3, right=5: 0+2=2 < 4 → left++
left=4, right=5: 1+2=3 < 4 → left++
left=5, right=5: 退出
i=1, nums[i]=-1, target=1
left=2, right=5: -1+2=1 == 1 ✓
添加 [-1, -1, 2]
left=3, right=4
left=3, right=4: 0+1=1 == 1 ✓
添加 [-1, 0, 1]
left=4, right=3: 退出
i=2, nums[i]=-1 (重复,跳过)
i=3, nums[i]=0 > 0, 退出
结果:[[-1, -1, 2], [-1, 0, 1]]
```
---
## 常见错误
### 错误1忘记排序
**错误代码**
```go
func threeSum(nums []int) [][]int {
// 直接遍历,没有排序
for i := 0; i < len(nums)-2; i++ {
// ...
}
}
```
**正确代码**
```go
func threeSum(nums []int) [][]int {
sort.Ints(nums) // 必须先排序
for i := 0; i < len(nums)-2; i++ {
// ...
}
}
```
**原因**:不排序无法使用双指针,无法去重
---
### 错误2去重逻辑不完整
**错误代码**
```go
// 只去重了第一个数
if i > 0 && nums[i] == nums[i-1] {
continue
}
// left和right没有去重
```
**正确代码**
```go
// 三个地方都要去重
if i > 0 && nums[i] == nums[i-1] {
continue
}
for left < right && nums[left] == nums[left+1] {
left++
}
for left < right && nums[right] == nums[right-1] {
right--
}
```
**原因**:避免重复结果
---
### 错误3指针移动条件错误
**错误代码**
```go
if sum < target {
right-- // 错误!应该增大和
}
```
**正确代码**
```go
if sum < target {
left++ // 正确增大left可以增大和
}
```
**原因**数组有序left越大和越大
---
### 错误4提前终止条件错误
**错误代码**
```go
if nums[i] >= 0 { // 错误等于0也要继续
break
}
```
**正确代码**
```go
if nums[i] > 0 { // 正确大于0才退出
break
}
```
**原因**[0, 0, 0] 是有效答案
---
## 进阶问题
### 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)