15. 3Sum

题目

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != j, i != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.

Notice that the solution set must not contain duplicate triplets.

Example 1:

1
2
3
4
5
6
7
8
Input: nums = [-1,0,1,2,-1,-4]
Output: [[-1,-1,2],[-1,0,1]]
Explanation:
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.
Different triplets are [-1,0,1] and [-1,-1,2].
Notice that the order of the output and the order of the triplets does not matter.

Example 2:

1
2
Input: nums = [0,1,1]
Output: []

Example 3:

1
2
Input: nums = [0,0,0]
Output: [[0,0,0]]

题目大意

在整数数组中找出所有不重复的三元组,使得三个数的和为 0。要求三元组不能重复,但顺序可以不同。


你选用何种方法解题?

本题的核心是高效查找三元组并去重。对于每个元素 nums[i],我们需要在剩余元素中找到两个数,使其和等于 -nums[i]

方法 时间复杂度 空间复杂度 是否推荐
排序 + 双指针 O(n²) O(log n) ~ O(n) 推荐
哈希表 O(n²) O(n) 不推荐
暴力枚举 O(n³) O(1) 不推荐

方法选择理由

  • 排序 + 双指针:通过排序实现 O(n log n) 的预处理,然后用双指针将内层查找优化到 O(n),整体 O(n²)。排序还能自然处理去重问题。
  • 哈希表:虽然也是 O(n²),但需要额外 O(n) 空间存储哈希表,且去重逻辑复杂,代码可读性差。
  • 暴力枚举:O(n³) 时间复杂度,无法通过较大规模测试用例。

解题过程

问题分析

输入:整数数组 nums
输出:所有不重复的三元组 [a, b, c],满足 a + b + c = 0

关键约束:三元组不能重复。例如 [-1, 0, 1][0, -1, 1] 视为相同。

核心洞察

  1. 排序的价值:排序后可以利用有序性进行双指针查找,同时便于跳过重复元素。
  2. 转化为两数之和:固定第一个数 nums[i],问题转化为在剩余元素中找两数之和为 -nums[i]
  3. 去重策略:排序后,相同元素会相邻,只需跳过与前一个相同的元素即可。

算法流程

nums = [-1, 0, 1, 2, -1, -4] 为例:

步骤 1:排序 → [-4, -1, -1, 0, 1, 2]

步骤 2:遍历每个元素作为第一个数:

i nums[i] 目标两数和 left right 当前和 操作
0 -4 4 1 5 (-1)+2=1 < 4 left++
0 -4 4 2 5 (-1)+2=1 < 4 left++
0 -4 4 3 5 0+2=2 < 4 left++
0 -4 4 4 5 1+2=3 < 4 left++(退出)
1 -1 1 2 5 (-1)+2=1 = 1 记录 [-1,-1,2],去重
1 -1 1 3 5 0+2=2 > 1 right--
1 -1 1 3 4 0+1=1 = 1 记录 [-1,0,1],去重
... ... ... ... ... ... ...

这些方法具体怎么运用?

方法:排序 + 双指针

数据结构:排序后的数组 + 两个指针(left, right)

核心逻辑

  1. 排序:先对数组排序,时间 O(n log n)
  2. 外层循环:遍历每个元素作为三元组的第一个数 nums[i]
    • nums[i] > 0,直接退出(排序后后面都是正数,不可能和为0)
    • 跳过重复的 nums[i](与前一个相同则跳过)
  3. 内层双指针:在 [i+1, n-1] 范围内查找两数之和为 -nums[i]
    • left = i + 1right = n - 1
    • nums[left] + nums[right] < target → left++
    • nums[left] + nums[right] > target → right--
    • 若等于 target → 记录结果,同时跳过两端的重复元素

边界情况处理

边界情况 处理方式
数组长度 < 3 直接返回空列表
第一个元素 > 0 排序后第一个正数后面都是正数,不可能和为0,直接退出
重复的第一个元素 if i > 0 and nums[i] == nums[i-1] 跳过
找到解后两端有重复元素 移动 left/right 直到指向不同的值
三个相同的元素如 [0,0,0] 正常处理,会记录一个三元组

复杂度

方法 时间复杂度 空间复杂度 说明
排序 + 双指针 O(n²) O(log n) ~ O(n) 排序 O(n log n) + 双指针 O(n²);空间取决于排序算法的实现
哈希表 O(n²) O(n) 需要额外哈希表存储已访问元素
暴力枚举 O(n³) O(1) 三重循环,无法通过大数据测试

常数因子分析:排序 + 双指针的实际运行速度通常优于哈希表,因为:

  • 排序后的数组具有更好的缓存局部性
  • 无需额外的哈希表操作开销
  • 去重逻辑更简洁

总结与最佳选择

最快算法排序 + 双指针。实际测试中,对于 n = 10⁴ 的输入,排序 + 双指针约 100ms,而哈希表方法约 200ms。

工程最优选择排序 + 双指针。理由如下:

  1. 时间效率高:O(n²) 时间复杂度,常数因子小
  2. 空间效率好:仅需排序所需的 O(log n) ~ O(n) 空间
  3. 代码简洁:去重逻辑自然融入排序后的遍历
  4. 稳定性强:不会受哈希冲突影响,性能稳定

各方法适用场景

  • 排序 + 双指针:首选方案,适用于绝大多数情况
  • 哈希表:仅在无法修改原数组顺序时考虑(本题不适用)
  • 暴力枚举:仅用于教学对比

Code

方法:排序 + 双指针(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from typing import List

class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
"""
排序 + 双指针法:
1. 先排序,便于双指针查找和去重
2. 固定第一个数,转化为两数之和问题
3. 用双指针在剩余区间查找满足条件的两个数
"""
n = len(nums)
result = []

# 步骤1:排序,O(n log n)
nums.sort()

# 步骤2:遍历每个元素作为三元组的第一个数
for i in range(n):
# 优化:第一个数大于0,后面都是正数,不可能和为0
if nums[i] > 0:
break

# 去重:跳过与前一个相同的元素
if i > 0 and nums[i] == nums[i-1]:
continue

# 步骤3:双指针查找两数之和为 -nums[i]
target = -nums[i]
left = i + 1
right = n - 1

while left < right:
current_sum = nums[left] + nums[right]

if current_sum == target:
# 找到解,记录三元组
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
elif current_sum < target:
# 和太小,需要更大的数
left += 1
else:
# 和太大,需要更小的数
right -= 1

return result

方法二:哈希表(不推荐,仅供对比)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from typing import List

class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
"""
哈希表法:固定第一个数,用哈希表存储已访问元素
注意:去重逻辑复杂,代码可读性差
"""
n = len(nums)
result = set() # 用集合去重

# 排序便于去重处理
nums.sort()

for i in range(n):
if nums[i] > 0:
break

# 跳过重复的第一个数
if i > 0 and nums[i] == nums[i-1]:
continue

seen = set()
for j in range(i + 1, n):
need = -nums[i] - nums[j]
if need in seen:
result.add((nums[i], need, nums[j]))
seen.add(nums[j])

# 转换为列表返回
return [list(triplet) for triplet in result]