在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

三数之和(LeetCode #15)

三数之和要求返回所有和为零、按值去重的三元组。先排序,固定一个锚点,再用双指针扫描后缀,将暴力 O(n³) 降为 O(n²)。重复值相邻,可以直接跳过,不必另建去重集合。

排序花 O(n log n),同时带来两个条件:左端候选最小、右端最大,指针排除规则成立;相等值挨在一起,锚点和两侧指针都能用邻居比较去重。

三数之和如何工作

固定候选三元组最小位置 nums[i] 为锚点,其余两值之和应为 need = -nums[i]。这就是在有序后缀上执行双指针搜索。lo 从锚点右侧开始,hi 从末尾开始。

和小于 need 时,当前左值连最大的可用右值都不够,推进 lo;和大于 need 时,当前右值连最小的可用左值都偏大,后退 hi。相等则记录 [nums[i], nums[lo], nums[hi]]。

三项细节都依赖有序性:

  • 跳过重复锚点:i > 0 且 nums[i] == nums[i - 1] 时,前一锚点已扫描更大的同目标后缀,本轮只能重复。
  • 命中后两端都移动,再跳过同值:固定锚点时,保持一端而让另一端变化,只可能因相同值再次命中,得到重复三元组。两端移动后,分别跳过刚使用值的连续副本。
  • 正锚点结束全部搜索:nums[i] > 0 时,后面都不更小,任何剩余三元组和都为正。使用 break,无需继续锚点循环。

逐步演示

默认 nums = [-1, 0, 1, 2, -1, -4]。

  1. 排序:得到 [-4, -1, -1, 0, 1, 2],两个 -1 相邻。
  2. 索引 0,锚点 -4,need = 4:依次检查 -1 + 2 = 1、-1 + 2 = 1、0 + 2 = 2、1 + 2 = 3。都不足 4,lo 走到与 hi 相遇,本锚点无结果。
  3. 索引 1,锚点 -1,need = 1:-1 + 2 = 1 命中,记录 [-1, -1, 2]。两指针内移到 0、1,没有重复跳过。
  4. 继续同一锚点:0 + 1 = 1,记录 [-1, 0, 1],两指针移动后交叉。一个锚点可以产生多组解。
  5. 索引 2 的 -1:等于左邻居,跳过。它能扫描的候选只是上一轮的子集。
  6. 索引 3,锚点 0,need = 0:唯一候选 1 + 2 = 3 > 0,hi 后退,指针相遇。锚点最多到 n - 3,之后不足三个位置。
  7. 完成:结果 [[-1, -1, 2], [-1, 0, 1]],每组恰好一次。

画布用紫色标锚点,标题显示 need,括号与双指针展示一次两数搜索。排除的格子变淡,锚点前的格子保留为已处理历史。每次配对检查让 tests 加一,最多 n 个锚点各扫描 O(n),形成 O(n²)。

默认数据不会触发命中后的左右去重循环,因为相邻值恰好不重复。切到配置中的 Duplicates everywhere,观察两端越过同值段;没有这两个循环就会再次记录同一组三元组。

时间与空间复杂度

方法时间扫描工作区,不含输出和另列成本去重
枚举每组三元组O(n³)O(1)若用规范化三元组集合,另需 O(k) 保存 k 个不同结果
排序、锚点、双指针O(n²)O(1)相邻跳过,无额外集合

排序 O(n log n) 被总扫描 O(n²) 覆盖。常数空间只指双指针层,不包含输出和排序工作区;Python Timsort 最坏可使用 O(n) 辅助空间。输入面板最多八个、范围 −20 到 20 的值,只为显示清楚。

目标不必为零,可改为 need = target - nums[i];提前结束条件也要改成 3 * nums[i] > target,因为锚点是三个值中最小的,三元组之和至少为 3 * nums[i]。保留 nums[i] > 0 的话,[1, 2, 3]、target 6 会在第一个锚点就停下,漏掉答案。k-sum 也可逐层固定锚点,递归到有序 2Sum,再用线性扫描结束。需要按值去重且允许排序时适合这套结构。nums.sort() 会原地改序;调用者需要原顺序时先复制。

边界情况

  • 恰好三个值:只有一个候选三元组:一次配对检查;若首个值为正,break 会在检查前结束。
  • 全为正:首锚点就触发正数 break,返回空列表。
  • 全为负:也无解,但不会触发正锚点条件;各轮因和不足而耗尽候选。
  • 全为零:[0, 0, 0, 0, 0] 有十组索引组合,但只返回一次 [0, 0, 0]。
  • 无解:正常返回空列表,不是错误。
  • 解两侧有重复:[-2, 0, 0, 1, 2, 2] 会触发两侧跳过,避免重复 [-2, 0, 2]。

常见错误

  • 不跳重复锚点。 默认第二个 -1 会再次找出 [-1, 0, 1]。
  • 命中后只动一端。 会重复检查只能以同值再次命中的组合。两端内移,再去重。
  • 已排序还依赖额外集合去重。 可以做,但增加 O(k) 存储与哈希;相邻规则已能得到规范化唯一结果。
  • 忽略原地排序。 需要保留原数组顺序时,排序副本。
  • 正锚点用 continue。 仍然正确,却继续处理已证明无解的锚点,break 才利用了有序性。
  • 两数和直接比较 0。 两数和应比较 need;若比较 0,必须把锚点也加进和里,不能混用公式。