LeetCode 15|三数之和:从暴力到双指针,一次性把“去重”讲清楚

7 阅读3分钟

三数之和(Three Sum)是一个非常经典、非常容易写错、也非常适合训练思维的题。

很多人第一次做这题的状态是:

  • 思路能想出来
  • 代码能跑
  • 结果一堆重复答案,直接 GG

这篇笔记,我会围绕一个核心目标来讲:

为什么一定要排序 + 双指针?
去重到底在去什么?


一、题目回顾

给你一个整数数组 nums,判断是否存在三个元素 a, b, c,使得:

a + b + c = 0

返回所有不重复的三元组

注意关键词:
不重复,这是这道题的灵魂。


二、为什么暴力解法不行?

最直观的写法是三层循环:

i + j + k == 0

问题有两个:

  1. 时间复杂度是 O(n³),数组稍微大一点就超时
  2. 完全没法优雅地去重

所以这道题的正确打开方式只有一条路:

先排序,再用双指针


三、排序的真正意义

排序不是为了好看,而是为了两件事:

  1. 让双指针成立
  2. 让去重变得可能

排序之后,数组满足:

nums[i] <= nums[left] <= nums[right]

这会带来一个非常重要的性质:

  • 当前和小了,只能让 left 右移
  • 当前和大了,只能让 right 左移

这是双指针成立的数学基础。


四、整体思路拆解

整体逻辑可以拆成三层:

  1. 固定第一个数 i
  2. i 右侧,用双指针找 left + right = -nums[i]
  3. 在三个层面上去重

五、完整代码

class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);

        int n = nums.length;
        for (int i = 0; i < n - 2; i++) {

            // 第一层去重:固定数 nums[i]
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }

            int left = i + 1;
            int right = n - 1;

            while (left < right) {
                int sum = nums[i] + nums[left] + nums[right];

                if (sum == 0) {
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));

                    // 第二层去重:left
                    while (left < right && nums[left] == nums[left + 1]) {
                        left++;
                    }

                    // 第三层去重:right
                    while (left < right && nums[right] == nums[right - 1]) {
                        right--;
                    }

                    left++;
                    right--;
                } else if (sum < 0) {
                    left++;
                } else {
                    right--;
                }
            }
        }
        return res;
    }
}

六、三层去重,逐层讲清楚

这是这道题最容易写错、也最值得理解的部分

1. 第一层去重:i 不能重复

if (i > 0 && nums[i] == nums[i - 1]) {
    continue;
}

含义是:

  • 如果当前固定的数和上一次固定的一样
  • 那后面的双指针结果一定也一样
  • 直接跳过,避免重复三元组

这是在防止这种情况:

[-1, -1, 0, 1]
 ↑   ↑
 i   i+1

2. 第二层去重:left 去重

while (left < right && nums[left] == nums[left + 1]) {
    left++;
}

当我们已经找到一个合法解:

nums[i] + nums[left] + nums[right] == 0

如果 left 指向的值后面还是一样的数:

... 0, 0, 0 ...
     ↑  ↑

继续用它只会得到一模一样的三元组

所以必须跳过。


3. 第三层去重:right 去重

while (left < right && nums[right] == nums[right - 1]) {
    right--;
}

逻辑和 left 完全对称。

你可以把这两层理解为一句话:

当前解已经用过了,这一整段相同的数都不再有价值


七、为什么要最后再 left++ / right--

left++;
right--;

前面的 while 只是“跳过重复值”,
但当前这对 (left, right) 已经用过了,必须整体向中间推进,继续找新解。


八、时间与空间复杂度

  • 时间复杂度:O(n²)

    • 外层一个 i
    • 内层双指针线性扫描
  • 空间复杂度:O(1)(不算结果集)

这是这道题能做到的最优解法。


九、总结一句话版

  • 排序是为了双指针和去重
  • 固定一个数,其余两个用左右指针夹逼
  • 去重一定要分三层,缺一不可

如果你能把**“为什么要去重、去的是什么重”**讲清楚,
那这道题你就真的吃透了。