vault backup: 2026-03-08 20:59:34
This commit is contained in:
4
.obsidian/workspace.json
vendored
4
.obsidian/workspace.json
vendored
@@ -198,6 +198,8 @@
|
||||
},
|
||||
"active": "fcbc762a80282002",
|
||||
"lastOpenFiles": [
|
||||
"16-LeetCode Hot 100/三数之和-改进版示例.md",
|
||||
"算法解题思路改进方案.md",
|
||||
"16-LeetCode Hot 100/单词搜索.md",
|
||||
"16-LeetCode Hot 100/从前序与中序遍历序列构造二叉树.md",
|
||||
"16-LeetCode Hot 100/除自身以外数组的乘积.md",
|
||||
@@ -227,8 +229,6 @@
|
||||
"16-LeetCode Hot 100/二叉树的中序遍历.md",
|
||||
"16-LeetCode Hot 100/最小栈.md",
|
||||
"16-LeetCode Hot 100/最长连续序列.md",
|
||||
"16-LeetCode Hot 100/完全平方数.md",
|
||||
"16-LeetCode Hot 100/最大正方形.md",
|
||||
"16-LeetCode Hot 100",
|
||||
"00-项目概述",
|
||||
"questions/15-简历面试",
|
||||
|
||||
730
16-LeetCode Hot 100/三数之和-改进版示例.md
Normal file
730
16-LeetCode Hot 100/三数之和-改进版示例.md
Normal file
@@ -0,0 +1,730 @@
|
||||
# 三数之和 (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/)
|
||||
386
算法解题思路改进方案.md
Normal file
386
算法解题思路改进方案.md
Normal file
@@ -0,0 +1,386 @@
|
||||
# 算法解题思路改进方案
|
||||
|
||||
## 当前状态分析
|
||||
|
||||
经过分析,现有LeetCode题目的解题思路已经包含了:
|
||||
- ✅ 核心思想
|
||||
- ✅ 算法流程
|
||||
- ✅ 复杂度分析
|
||||
- ✅ 代码实现
|
||||
- ✅ 部分题目的图解
|
||||
|
||||
## 改进方向
|
||||
|
||||
### 1. 解题思路部分需要增强
|
||||
|
||||
**改进前** (以三数之和为例):
|
||||
```markdown
|
||||
### 核心思想
|
||||
**排序 + 双指针**:先排序,固定第一个数,再用双指针找后两个数。
|
||||
|
||||
### 算法流程
|
||||
1. **排序数组**:便于去重和双指针操作
|
||||
2. **遍历第一个数**:
|
||||
- 跳过重复元素
|
||||
- 如果当前数 > 0,直接退出
|
||||
3. **双指针找后两个数**:
|
||||
- left = i + 1, right = len(nums) - 1
|
||||
- 根据 sum 与 0 的关系移动指针
|
||||
```
|
||||
|
||||
**改进后**:
|
||||
```markdown
|
||||
### 核心思想
|
||||
**排序 + 双指针**:先排序,固定第一个数,再用双指针找后两个数。
|
||||
|
||||
**为什么这样思考?**
|
||||
1. **排序的作用**:
|
||||
- 去除重复结果(相同数相邻)
|
||||
- 使数组有序,才能使用双指针
|
||||
- 提前终止(如果当前数>0,后面都>0)
|
||||
|
||||
2. **双指针的原理**:
|
||||
- 数组有序后,如果 sum < target,需要增大 → left++
|
||||
- 如果 sum > target,需要减小 → right--
|
||||
- 利用单调性,避免暴力枚举
|
||||
|
||||
3. **降维思想**:
|
||||
- 三数之和 → 固定一个数 → 两数之和
|
||||
- O(n³) → O(n²)
|
||||
|
||||
### 解题思路推导过程
|
||||
|
||||
**第一步:暴力解法分析**
|
||||
```
|
||||
暴力解法:三层循环枚举所有可能
|
||||
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:
|
||||
result.add([nums[i], nums[j], nums[k]])
|
||||
|
||||
时间复杂度: O(n³)
|
||||
空间复杂度: O(1)
|
||||
问题:效率太低,无法通过
|
||||
```
|
||||
|
||||
**第二步:优化思考 - 能否降维?**
|
||||
- 观察到固定第一个数后,问题变成"两数之和"
|
||||
- 两数之和可以用双指针 O(n) 解决
|
||||
- 总复杂度: O(n) × O(n) = O(n²)
|
||||
|
||||
**第三步:双指针的前提条件**
|
||||
- 为什么排序后才能用双指针?
|
||||
- 如果 nums[left] + nums[right] < 0
|
||||
- 由于数组升序,增大left → 和会变大
|
||||
- 减小right → 和会变小
|
||||
- 这就是"单调性"的作用
|
||||
|
||||
### 详细算法流程
|
||||
|
||||
**步骤1:预处理 - 排序**
|
||||
```python
|
||||
nums.sort() # O(n log n)
|
||||
```
|
||||
- 作用:去重、双指针基础、提前终止
|
||||
|
||||
**步骤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
|
||||
|
||||
# 双指针找后两个数
|
||||
twoSum(nums, i+1, -nums[i])
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- 为什么循环到 `len(nums)-2`? 需要留2个数给双指针
|
||||
- 为什么判断 `i > 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时会越界!
|
||||
|
||||
# 正确写法
|
||||
if i > 0 and nums[i] == nums[i-1]: # 第一个元素不用判断
|
||||
```
|
||||
|
||||
**细节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 # 错误:会继续无意义的循环
|
||||
```
|
||||
```
|
||||
### 边界条件分析
|
||||
|
||||
**边界1:数组长度不足**
|
||||
```
|
||||
输入: [0, 1]
|
||||
输出: []
|
||||
原因:长度<3,无法组成三元组
|
||||
```
|
||||
|
||||
**边界2:全部为0**
|
||||
```
|
||||
输入: [0, 0, 0, 0]
|
||||
输出: [[0, 0, 0]]
|
||||
去重逻辑:只保留一个组合
|
||||
```
|
||||
|
||||
**边界3:有重复元素**
|
||||
```
|
||||
输入: [-1, -1, 0, 1]
|
||||
输出: [[-1, 0, 1]]
|
||||
去重逻辑:跳过第二个-1
|
||||
```
|
||||
|
||||
### 复杂度分析(详细版)
|
||||
|
||||
**时间复杂度**:
|
||||
```
|
||||
- 排序: 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) (不考虑结果存储)
|
||||
```
|
||||
```
|
||||
|
||||
### 2. 增加思考过程部分
|
||||
|
||||
在"解题思路"之前,增加"思路推导"部分:
|
||||
- 从暴力解法开始
|
||||
- 分析暴力解法的瓶颈
|
||||
- 逐步优化思路
|
||||
- 最终得到最优解
|
||||
|
||||
### 3. 增加图解说明
|
||||
|
||||
为每个关键步骤添加:
|
||||
- 初始状态图
|
||||
- 中间过程图
|
||||
- 最终结果图
|
||||
- 使用ASCII art或文字描述
|
||||
|
||||
### 4. 增加常见错误
|
||||
|
||||
列出易错点:
|
||||
- 边界条件错误
|
||||
- 去重逻辑错误
|
||||
- 指针移动条件错误
|
||||
- 提前终止条件错误
|
||||
|
||||
### 5. 增加变体问题
|
||||
|
||||
扩展到相关题目:
|
||||
- 参数变化(四数之和)
|
||||
- 条件变化(最接近的和)
|
||||
- 返回值变化(返回索引而非值)
|
||||
|
||||
## 改进后的模板
|
||||
|
||||
```markdown
|
||||
# [题目名称]
|
||||
|
||||
LeetCode [题号]. [难度]
|
||||
|
||||
## 题目描述
|
||||
[题目原文]
|
||||
|
||||
## 思路推导
|
||||
|
||||
### 暴力解法分析
|
||||
```python
|
||||
[暴力解法代码]
|
||||
```
|
||||
**时间复杂度**: O(?)
|
||||
**问题**: [分析瓶颈]
|
||||
|
||||
### 优化思考
|
||||
**观察**: [发现规律或可优化点]
|
||||
**思路**: [优化方向]
|
||||
|
||||
### 为什么这样思考?
|
||||
1. [核心原理1]
|
||||
2. [核心原理2]
|
||||
3. [降维/转换思想]
|
||||
|
||||
## 解题思路
|
||||
|
||||
### 核心思想
|
||||
[一句话总结]
|
||||
|
||||
### 算法流程(详细版)
|
||||
|
||||
**步骤1:[步骤名称]**
|
||||
```
|
||||
[图解或示例]
|
||||
```
|
||||
- 关键点1:[说明]
|
||||
- 关键点2:[说明]
|
||||
|
||||
**步骤2:[步骤名称]**
|
||||
[...]
|
||||
|
||||
### 关键细节说明
|
||||
|
||||
**细节1:[细节名称]**
|
||||
```python
|
||||
[代码示例]
|
||||
```
|
||||
[为什么这样写]
|
||||
|
||||
**细节2:[细节名称]**
|
||||
[...]
|
||||
|
||||
### 边界条件分析
|
||||
|
||||
**边界1:[条件名称]**
|
||||
```
|
||||
输入:[示例]
|
||||
输出:[示例]
|
||||
处理:[说明]
|
||||
```
|
||||
|
||||
### 复杂度分析(详细版)
|
||||
|
||||
**时间复杂度**:
|
||||
```
|
||||
[计算过程]
|
||||
总计:O(?)
|
||||
```
|
||||
|
||||
**空间复杂度**:
|
||||
```
|
||||
[计算过程]
|
||||
总计:O(?)
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
```python
|
||||
[完整代码,包含详细注释]
|
||||
```
|
||||
|
||||
## 执行过程演示
|
||||
|
||||
```
|
||||
[示例输入的完整执行过程]
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
### 错误1:[错误名称]
|
||||
❌ 错误写法:
|
||||
```python
|
||||
[错误代码]
|
||||
```
|
||||
✅ 正确写法:
|
||||
```python
|
||||
[正确代码]
|
||||
```
|
||||
**原因**:[说明]
|
||||
|
||||
## 变体问题
|
||||
|
||||
### 变体1:[变体描述]
|
||||
[解法思路]
|
||||
|
||||
## 总结
|
||||
|
||||
**核心要点**:
|
||||
1. [要点1]
|
||||
2. [要点2]
|
||||
3. [要点3]
|
||||
|
||||
**易错点**:
|
||||
- [易错点1]
|
||||
- [易错点2]
|
||||
```
|
||||
|
||||
## 实施建议
|
||||
|
||||
1. **优先级**:先完善高频题目(Hot 100)
|
||||
2. **分阶段**:
|
||||
- 第一阶段:增加"思路推导"部分
|
||||
- 第二阶段:增加"执行过程演示"
|
||||
- 第三阶段:增加"常见错误"
|
||||
|
||||
3. **保持一致性**:所有题目使用统一模板
|
||||
|
||||
4. **可读性优先**:
|
||||
- 使用代码块突出关键部分
|
||||
- 使用列表提高可读性
|
||||
- 适当使用emoji(谨慎使用)
|
||||
|
||||
## 示例对比
|
||||
|
||||
查看 `三数之和-改进版.md` 了解改进后的完整效果。
|
||||
Reference in New Issue
Block a user