算法困惑问答 · LC 15 三数之和
三数之和怎么去重,才能既不重复也不漏解?
直接答案
先排序,然后在三个位置分别「跳过相邻重复值」:① 固定的第一个数 i,与前一个相同就跳过整轮;② 命中一组解后,l 右移时跳过与刚才相同的值;③ r 左移时同样跳过。三处缺一处就会输出重复三元组。去重的本质是:排序让相同的数挨在一起,「和上一个相同的候选」产生的解集完全一样,直接跳过零损失、也不会漏。
为什么必须先排序
两个原因绑在一起:对撞双指针靠「和大了动 r、和小了动 l」的单调性运转,乱序时这个判断没有依据;同时排序把相同的数排到一起,「跳过相邻重复」这种 O(1) 的去重才有可能。不排序想去重,就得拿哈希 set 存排序后的三元组做键,代码更臃肿、常数更大。
整体流程:排序后固定 i 从左往右扫(i 处的数与前一个相同就 continue),对每个 i 在 (i, n) 区间上做对撞:sum 大了 r--,小了 l++,等于目标就记录。
命中之后:同时内移 + 各自跳重
命中一组解后,只动一个指针是经典 bug:只右移 l,同一个 r 会和新的相同值再配一次,反复命中同一组。正确动作是 l++ 且 r-- 同时向内,然后 l 跳过与前一个相同的值、r 跳过与后一个相同的值。
有人担心「跳重会不会漏解」:不会。以 l 为例,跳过的那些值和刚才命中的 l 值相同,配同一个 i 时需要的另一半也相同,能组成的三元组集合一模一样——跳过的只是重复劳动,不是新答案。
代码关键行(Python)
nums.sort()
for i in range(len(nums) - 2):
if i > 0 and nums[i] == nums[i-1]:
continue # 去重 1:固定位跳重
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0: l += 1
elif s > 0: r -= 1
else:
res.append([nums[i], nums[l], nums[r]])
l += 1; r -= 1 # 命中后同时内移
while l < r and nums[l] == nums[l-1]: l += 1 # 去重 2
while l < r and nums[r] == nums[r+1]: r -= 1 # 去重 3三处去重各司其职:`i` 防止整轮重复,`l`、`r` 防止同一轮内重复。注意比较对象是「刚离开的那个位置」(l-1、r+1),写反了会把合法解跳掉。
常见追问
直接把结果丢进 set 去重不行吗?
能过,但要把每组解转成可哈希的元组存起来,多一份 O(k) 空间,且掩盖了「为什么会产生重复」的理解。面试里手写三处跳重是更被认可的答案。
四数之和怎么去重?
同一套思路再套一层:固定两个数(各自跳相邻重复)+ 内层对撞双指针(命中后 l、r 跳重),复杂度 O(n³)。去重原则不变:每个「层级」的候选都跳过相邻重复值。
把这道题彻底吃透
LC 15「三数之和」有逐步交互动画和完整图文题解,配着本页的结论看,一遍就通。