LeetCode 15. 三数之和

题目描述

给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]],满足 i != ji != kj != k,并且:

复制代码
nums[i] + nums[j] + nums[k] == 0

返回所有和为 0 且不重复的三元组。答案中不能包含重复的三元组。

最初思路

最开始的想法是先排序,然后外层循环固定一个数 nums[k],把目标值设为:

复制代码
t = -nums[k]

内层从 k + 1 往后遍历,设当前数为 nums[j],再通过:

复制代码
m.contains(t - nums[j])

判断第三个数是否已经出现过。如果存在,就把 nums[j]t - nums[j]nums[k] 加入答案。

这个方向抓住了"三数之和可以转成两数之和"的形式,但 m 里只保存了值,没有保存下标信息。

问题出在哪里

三数之和不仅要求值相加为 0,还要求三个数来自三个不同下标。

原思路里的 m.contains(...) 只能回答"这个值是否出现过",不能回答"这个值来自哪个下标"。例如:

复制代码
nums = [-2, 4, 5]

当固定 nums[k] = -2,内层遇到 nums[j] = 4 时:

复制代码
t = 2
t - nums[j] = -2

如果 m 里已经加入了当前固定的 -2,那么 contains(-2) 会成立,但这个 -2 可能就是 nums[k] 自己。这样就无法证明三个数来自不同位置。

还有一个问题是重复三元组。排序后相同数字会连续出现,如果外层起点相同,后续可能再次得到相同的值组合。只判断值是否存在,不足以处理答案去重。

正确思路

先排序,再固定第一个数 x = nums[i],然后在 i 右侧使用双指针寻找另外两个数:

  • j = i + 1,指向当前候选区间左端。
  • k = n - 1,指向当前候选区间右端。
  • 每轮计算 s = x + nums[j] + nums[k]
  • 如果 s > 0,说明右侧数字太大,移动 k--
  • 如果 s < 0,说明左侧数字太小,移动 j++
  • 如果 s == 0,记录答案,然后同时移动 j++k--

因为 jk 都从 i 右侧开始,所以可以直接保证三个下标不同:

复制代码
i < j < k

核心洞察

排序以后,数组从小到大排列。固定 x 后,问题就变成在右侧有序区间里找两个数,使它们的和等于 -x

如果当前三数之和太大,就需要让总和变小,只能移动右指针 k;如果当前三数之和太小,就需要让总和变大,只能移动左指针 j

去重也依赖排序:

  • 外层如果 nums[i] == nums[i - 1],说明这个起点已经处理过,直接跳过。
  • 内层找到答案后,如果当前 nums[j] 和上一次使用的 nums[j - 1] 相同,就不重复加入答案。

固定 i 后,如果:

复制代码
x + nums[j] + nums[k] == 0

那么:

复制代码
nums[k] = -x - nums[j]

所以同一个 i 下,只要 nums[j] 没变,nums[k] 的值组合也被唯一确定,重复的 j 会导致重复三元组。

手推过程

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

复制代码
[-4, -1, -1, 0, 1, 2]

先固定 i = 0

复制代码
x = -4
j = 1, k = 5
s = -4 + (-1) + 2 = -3

总和偏小,移动 j。这一轮不会得到答案。

再固定 i = 1

复制代码
x = -1
j = 2, k = 5
s = -1 + (-1) + 2 = 0

得到 [-1, -1, 2],然后移动 jk

继续:

复制代码
j = 3, k = 4
s = -1 + 0 + 1 = 0

得到 [-1, 0, 1]

i = 2 时,nums[2] == nums[1],这个起点已经处理过,直接跳过,避免重复答案。

伪代码

下面的伪代码与后面的 Java 实现一一对应:

复制代码
函数 threeSum(nums):
    对 nums 排序
    ans <- 空列表
    n <- nums.length

    对 i 从 0 到 n - 3:
        x <- nums[i]

        如果 i > 0 且 nums[i] == nums[i - 1]:
            跳过当前 i

        如果 x + nums[i + 1] + nums[i + 2] > 0:
            结束循环

        如果 x + nums[n - 2] + nums[n - 1] < 0:
            跳过当前 i

        j <- i + 1
        k <- n - 1

        当 j < k:
            s <- x + nums[j] + nums[k]

            如果 s > 0:
                k <- k - 1
            否则如果 s < 0:
                j <- j + 1
            否则:
                如果 j == i + 1 或 nums[j] != nums[j - 1]:
                    把 [x, nums[j], nums[k]] 加入 ans
                j <- j + 1
                k <- k - 1

    返回 ans

Java 代码

复制代码
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < n - 2; i++) {
            int x = nums[i];
            if (i > 0 && x == nums[i - 1]) continue;
            if (x + nums[i + 1] + nums[i + 2] > 0) break;
            if (x + nums[n - 2] + nums[n - 1] < 0) continue;

            int j = i + 1;
            int k = n - 1;
            while (j < k) {
                int s = x + nums[j] + nums[k];
                if (s > 0) {
                    k--;
                } else if (s < 0) {
                    j++;
                } else {
                    if (j == i + 1 || nums[j] != nums[j - 1]) {
                        ans.add(List.of(x, nums[j], nums[k]));
                    }
                    j++;
                    k--;
                }
            }
        }
        return ans;
    }
}

易错点

  1. 只用 contains 判断值是否存在,不足以证明三个数来自不同下标。
  2. mSet 如果跨外层循环共享,里面会混入历史状态,候选范围不清晰。
  3. 外层固定值要去重:i > 0 && nums[i] == nums[i - 1] 时直接跳过。
  4. 找到答案后要继续移动指针,否则会卡在同一组位置上。
  5. 同一个 i 下,重复的 nums[j] 会得到重复值组合,需要避免重复加入。

建议测试用例

复制代码
nums = [-1, 0, 1, 2, -1, -4]
期望:[[-1, -1, 2], [-1, 0, 1]]
说明:常规重复数字场景

nums = [0, 0, 0, 0]
期望:[[0, 0, 0]]
说明:检查重复答案去重

nums = [-2, 0, 1, 1, 2]
期望:[[-2, 0, 2], [-2, 1, 1]]
说明:同一个起点下存在多个答案

nums = [-2, 4, 5]
期望:[]
说明:检查是否错误复用固定元素本身

复杂度分析

  • 时间复杂度:O(n^2)。排序是 O(n log n),主循环中每个 i 对应一次线性双指针扫描,总体为 O(n^2)
  • 空间复杂度:O(1)。不计算返回答案;如果计算排序递归栈,空间复杂度与语言排序实现有关。

总结

这道题的关键是把"找三个数"拆成"固定一个数,再在右侧找两个数"。

原来的 contains 思路只关注值是否存在,没有管理下标范围;双指针做法通过 i < j < k 直接保证下标不同,再借助排序处理重复值。

下次遇到"不重复组合"时,可以先问自己两个问题:候选元素的下标范围是否清楚?相同值连续出现时,哪一层循环负责去重?

相关推荐
AiNightVision1 小时前
NMC存算一体与AI ISP
人工智能·算法·车载系统·自动驾驶·无人机·视频·智能硬件
不穿鞋的懒羊羊1 小时前
高精度算法——加、减、乘、除
算法
liliangcsdn1 小时前
基本面因子计算的示例分析
算法
Lyyaoo.2 小时前
【动态规划】【待更新】
java·数据结构·算法
AgentMaster2 小时前
开源智能客服系统技术对比:从架构设计到生产部署的实战指南
大数据·人工智能·算法
weixin_307779132 小时前
C++代码实现MATLAB中的ode23t函数功能
开发语言·c++·算法·matlab
萧西待水2 小时前
奥赛一本通 1451 棋盘游戏
算法·宽度优先
Niuguangshuo2 小时前
论文解读:Paraformer,非自回归中文 ASR 的并行 Transformer
算法·音视频·语音识别
鹿角片ljp3 小时前
LeetCode 78:子集|回溯、选与不选、递归和path快照
java·数据结构·算法