LeetCode 15 三数之和:python3 题解
题目链接:15. 三数之和
目录
1. 题目理解
目标:在一个整数数组 nums 中,找到所有不重复的三元组 [a, b, c],使得 a + b + c = 0。
关键约束:
- 下标不同:三个数必须来自数组中不同的位置。
- 结果不重复:例如
[-1, 0, 1]和[0, -1, 1]被视为同一个三元组,结果中只能出现一次。 - 效率:数组长度最大为 3000,这意味着 \(O(N^3)\) 的暴力解法会超时,我们需要 \(O(N^2)\) 或更优的解法。
2. 解题思路演变
为了更好地理解最优解,我们先从最直观的想法开始,逐步优化。
思路一:暴力枚举(不可行)
最直观的方法是使用三层循环:
- 第一层选第一个数
nums[i]。 - 第二层选第二个数
nums[j]。 - 第三层选第三个数
nums[k]。 - 判断和是否为 0,并用集合(Set)去重。
- 时间复杂度:\(O(N^3)\)。当 \(N=3000\) 时,运算次数约为 270 亿次,必然超时 (Time Limit Exceeded)。
- 去重难点:暴力法去重非常麻烦,需要额外存储已见过的组合。
思路二:排序 + 双指针(最优解)
这是解决此类问题(K-Sum)的标准范式。核心思想是将问题转化为 “固定一个数,寻找两数之和”。
为什么要排序?
- 方便去重:排序后,相同的数字会挨在一起。如果我们发现当前数字和前一个数字一样,就可以直接跳过,从根源上避免重复三元组。
- 使用双指针:排序后,数组有序。如果当前和太小,我们可以确定地向右移动指针来增大和;如果和太大,向左移动指针来减小和。这避免了第三层循环。
算法步骤
- 排序:将数组
nums从小到大排序。 - 遍历第一个数:用索引
i遍历数组,nums[i]作为三元组的第一个数。- 剪枝优化:如果
nums[i] > 0,因为数组已排序,后面的数都大于 0,三数之和不可能为 0,直接结束循环。 - 去重:如果
i > 0且nums[i] == nums[i-1],说明这个数字作为第一个数已经处理过了,跳过(continue)。
- 剪枝优化:如果
- 双指针寻找后两个数:
- 定义左指针
left = i + 1,右指针right = len(nums) - 1。 - 当
left < right时循环:- 计算总和
s = nums[i] + nums[left] + nums[right]。 - 如果
s == 0:找到一组解!- 加入结果集。
- 移动
left和right。 - 关键去重:移动后,如果
nums[left]和刚才的值一样,继续右移;如果nums[right]和刚才的值一样,继续左移。防止重复解。
- 如果
s < 0:和太小,需要变大,left右移。 - 如果
s > 0:和太大,需要变小,right左移。
- 计算总和
- 定义左指针
3. 代码实现 (Python 3)
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
# 1. 基础边界检查:如果数组少于 3 个数,不可能组成三元组
if not nums or len(nums) < 3:
return []
# 2. 排序:这是使用双指针和方便去重的前提
# 时间复杂度 O(N log N)
nums.sort()
n = len(nums)
result = []
# 3. 遍历第一个数 nums[i]
# 只需要遍历到 n-2,因为后面至少要留两个数给 left 和 right
for i in range(n - 2):
# --- 剪枝优化 ---
# 如果当前数字大于 0,由于数组已排序,后面的数字也都大于 0
# 三数之和一定大于 0,不可能等于 0,直接结束循环
if nums[i] > 0:
break
# --- 第一个数去重 ---
# 如果当前数字和前一个数字相同,说明作为第一个数的情况已经处理过了
# 跳过本次循环,避免重复三元组
if i > 0 and nums[i] == nums[i - 1]:
continue
# 4. 双指针寻找剩下的两个数
# left 指向 i 的下一个位置,right 指向数组末尾
left = i + 1
right = n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total == 0:
# 找到满足条件的三元组
result.append([nums[i], nums[left], nums[right]])
# --- 第二、三个数去重 ---
# 找到解后,移动指针前,先跳过所有重复的 left 和 right 值
# 例如:[-2, 0, 0, 2, 2],找到 [-2, 0, 2] 后,跳过中间的 0 和 2
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
elif total < 0:
# 和太小,需要增大,左指针右移
left += 1
else:
# 和太大,需要减小,右指针左移
right -= 1
return result
4. 图解演示
假设输入 nums = [-1, 0, 1, 2, -1, -4]。
- 排序后:
[-4, -1, -1, 0, 1, 2] - 第一轮 (i=0):
nums[i] = -4left指向-1(索引 1),right指向2(索引 5)。- 和 = -4 + (-1) + 2 = -3 < 0。
left右移。 - ... 经过多次移动,无法找到和为 0 的组合(因为 -4 太小了)。
- 第二轮 (i=1):
nums[i] = -1left指向-1(索引 2),right指向2(索引 5)。- 和 = -1 + (-1) + 2 = 0。找到解
[-1, -1, 2]。 - 记录结果,移动指针并去重。
- 第三轮 (i=2):
nums[i] = -1- 发现
nums[2] == nums[1],跳过(去重逻辑生效)。
- 发现
- 第四轮 (i=3):
nums[i] = 0left指向1,right指向2。- 和 = 0 + 1 + 2 = 3 > 0。
right左移。 left指向1,right指向1。循环结束。- (注:实际逻辑中会找到
[-1, 0, 1],此处为简化演示)。
5. 复杂度分析
- 时间复杂度: \(O(N^2)\)
- 排序消耗 \(O(N \log N)\)。
- 外层循环遍历 \(N\) 次,内层双指针遍历 \(N\) 次。总共 \(N \times N = N^2\)。
- \(N^2\) 远大于 \(N \log N\),所以主导项是 \(O(N^2)\)。
- 对于 \(N=3000\),\(N^2 = 9 \times 10^6\),完全可以在 1 秒内完成。
- 空间复杂度: \(O(1)\) 或 \(O(N)\)
- 取决于排序算法的实现(Python 的
sort通常是 Timsort,空间复杂度 \(O(N)\))。 - 如果不算存储结果的空间,算法本身只用了几个指针变量,是常数级别。
- 取决于排序算法的实现(Python 的
6. 常见坑点与注意事项
- 去重逻辑的位置:
- 外层循环去重:必须在
i > 0时判断nums[i] == nums[i-1]。不能在i=0时判断,否则i-1越界。 - 内层指针去重:必须在找到
total == 0之后 进行。如果在total != 0时去重,可能会漏掉某些组合(虽然在这个特定题目逻辑下通常不会,但标准写法是在找到解后跳过重复值)。
- 外层循环去重:必须在
- 指针移动顺序:
- 在找到解后,先执行
while循环跳过重复值,最后再执行left += 1和right -= 1指向新的值。如果顺序反了,逻辑会变复杂。
- 在找到解后,先执行
- 剪枝:
if nums[i] > 0: break这个优化非常关键,能大幅减少不必要的计算,特别是在数组中有很多正数的时候。
7. 总结
这道题是面试中非常经典的数组双指针题目。
- 核心技巧:排序 + 双指针。
- 核心难点:如何优雅地去除重复三元组。

浙公网安备 33010602011771号