# 三数之和 (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/)