LC 15 三数之和:双指针不难,难的是把去重做对

有没有人和我一样? 刚学双指针的时候,觉得两数之和简直手到擒来,直到遇上三数之和。 思路我都懂啊,不就是固定一个数,剩下两个用双指针嘛。 结果一写代码,要么超时,要么一堆重复结果,改了三四遍都没过,给我整懵了。

题目说的啥

其实题意特别简单: 给你一个整数数组,找出所有下标不同的三元组,让三个数加起来等于 0。 唯一的坑就是,结果里不能有重复的三元组 ------ 元素一样、顺序不一样也算重复。

暴力法:想都不用想,肯定超时

最开始我图省事,直接三重循环枚举所有组合。 写完跑了下示例,好像还挺对? 一提交直接超时。 毕竟数组长度最多 3000,O (n³) 的复杂度根本顶不住,而且手动去重还特别麻烦,写着写着自己都绕晕了。

双指针才是这题的正确打开方式

后来我反应过来,两数之和能用双指针,三数之和不就是多固定一个数吗? 不过双指针有个前提:数组必须有序。所以第一步,先给数组排序。

整体逻辑其实很顺: 遍历数组里的每个数,把它当成三元组的第一个数。 然后在它右边的区间里,用左指针指向开头,右指针指向结尾,找两个数加起来等于第一个数的相反数。 和太小就左指针右移,和太大就右指针左移,刚好等于 0 就存起来。

听起来是不是很简单? 但我当初就是在 "存起来" 之后的步骤里,翻了大车。 而且跑多了测试用例我发现,其实还有很多没必要的循环,可以提前剪枝跳过,能让代码快上不少。

两个必加的剪枝优化

这两个优化我一开始没写,后来看了更精炼的写法才反应过来,加上之后运行时间直接砍半。

第一个:如果当前固定的数,加上它后面最小的两个数,和都已经大于 0 了。 那后面不管怎么组合,和只会更大,不可能等于 0,直接 break 跳出循环就行。

第二个:如果当前固定的数,加上数组最后最大的两个数,和都还是小于 0。 说明当前这个数太小了,怎么凑都凑不到 0,直接跳过,去看下一个数。

去重的细节一个都不能少

这题真正的考点根本不是双指针,是去重。 三个地方要去重,少一个都过不了。

第一个,遍历第一个数的时候要去重。 如果当前数和前一个数一模一样,直接跳过。 不然数组里有两个相同的数,分别当第一个数的时候,找出来的三元组肯定是重复的。

第二个和第三个,找到符合条件的三元组后,左右指针都要去重。 这里我把写法简化成了一行 for 循环,移动指针的同时直接跳过重复值,不用分开写好几行,干净很多。

拿示例 1 走一遍流程

光说细节有点干,咱们拿示例 1 手算一遍,一下子就通透了。

示例输入是 [-1,0,1,2,-1,-4],第一步先排序,得到: [-4, -1, -1, 0, 1, 2]

我们一步步来看:

第一轮:i = 0,固定值 x = -4 左指针 j 从 1 开始(值 -1),右指针 k 从 5 开始(值 2)。 三数之和 = -4 + (-1) + 2 = -3 < 0,和太小了,j 右移。 j=2(值 -1):和 = -4 + (-1) + 2 = -3 < 0,j 继续右移。 j=3(值 0):和 = -4 + 0 + 2 = -2 < 0,j 继续右移。 j=4(值 1):和 = -4 + 1 + 2 = -1 < 0,j 继续右移。 j=5,和 k 重合了,本轮结束,没找到符合条件的三元组。

第二轮:i = 1,固定值 x = -1 左指针 j = 2(值 -1),右指针 k = 5(值 2)。 三数之和 = -1 + (-1) + 2 = 0,刚好等于 0! 我们把 [-1, -1, 2] 加入结果。 然后开始去重:j 右移,下一个是 0,和之前的 -1 不一样,停在 j=3; k 左移,下一个是 1,和之前的 2 不一样,停在 k=4。

现在 j=3(值 0),k=4(值 1),继续算: 和 = -1 + 0 + 1 = 0,又等于 0! 把 [-1, 0, 1] 加入结果。 继续去重移动指针,j 和 k 相遇,本轮结束。

第三轮:i = 2,固定值 x = -1 这时候要注意,nums 2 和 nums 1 都是 -1,重复了! 直接执行 continue 跳过,避免生成重复的三元组。

后面 i=3、i=4 的时候,要么和已经大于 0,要么指针不够用,循环就结束了。

最后得到的结果就是 [[-1,-1,2], [-1,0,1]],和示例输出完全对上。

代码实现

把这些细节都揉进去,最终的代码就非常精炼了。 我把每个坑和优化点都写在注释里了,你们对着看就行。

javascript 复制代码
var threeSum = function(nums) {
    nums.sort((a, b) => a - b);
    const n = nums.length;
    const ans = [];
    
    // 只需要遍历到倒数第三个,后面留两个数给双指针
    for (let i = 0; i < n - 2; i++) {
        const x = nums[i];
        // 第一个数去重,和前一个相同就跳过,别写错成后一个
        if (i > 0 && x === nums[i - 1]) continue;
        
        // 优化一:当前数 + 后面最小的两个数都 >0,后续只会更大,直接结束循环
        if (x + nums[i + 1] + nums[i + 2] > 0) break;
        // 优化二:当前数 + 数组最大的两个数都 <0,当前数太小,直接跳过本轮
        if (x + nums[n - 2] + nums[n - 1] < 0) continue;
        
        let j = i + 1, k = n - 1;
        while (j < k) {
            const s = x + nums[j] + nums[k];
            if (s > 0) {
                k--;
            } else if (s < 0) {
                j++;
            } else {
                // 找到符合条件的三元组,加入结果集
                ans.push([x, nums[j], nums[k]]);
                // 左指针去重:先右移一位,再跳过所有重复值
                for (j++; j < k && nums[j] === nums[j - 1]; j++);
                // 右指针去重:先左移一位,再跳过所有重复值
                for (k--; k > j && nums[k] === nums[k + 1]; k--);
            }
        }
    }
    return ans;
};

复杂度怎么样

渐进时间复杂度还是 O (n²),排序是 O (nlogn),外层循环加双指针遍历是 O (n²)。 但加了两个剪枝之后,实际运行时会跳过大量无效循环,在大数据量的测试用例下表现会好很多。 空间复杂度不算结果数组的话,就是排序占用的 O (logn)。

最后唠两句

回头看这题,其实真不算难。 核心就是排序 + 固定一个数 + 双指针,套路很固定。 但就是去重的几个小细节,还有剪枝的思路,特别容易忽略,我当初卡了快半小时才把所有用例跑通。

刷这种题最忌讳的就是 "一看就会,一写就废",觉得思路懂了就不动手。 真自己敲一遍代码,才知道哪里藏着坑。

你当初刷三数之和的时候,是被哪个点卡住了? 是双指针写不对,还是去重一直有问题? 评论区聊聊你的踩坑经历,我每条都会看~

如果觉得这篇题解对你有帮助,点个赞让更多刷题的朋友看到就更好啦。

相关推荐
触底反弹2 小时前
🔥 前端也能玩转 AI 流式输出!从二进制流到打字机效果,一篇讲透
javascript·人工智能·node.js
胡萝卜术2 小时前
深度重构:Agent Skills——从 Prompt 工程到能力工程
javascript·架构·github
renhongxia13 小时前
世界模型,是“空中楼阁”还是AGI的“最后一块拼图”?
运维·服务器·数据库·人工智能·算法·agi
G.O.G.O.G4 小时前
LeetCode SQL 从入门到精通(MySQL)06(上)
数据库·sql·mysql·leetcode
To_OC4 小时前
从 “卡死半天” 到 “打字机效果”:大模型流式输出前端实现全记录
前端·javascript·llm
rockey6274 小时前
基于AScript的JavaScript脚本语言发布啦
javascript·c#·.net·js·script
kyriewen5 小时前
我看完这篇安全论文——AI推荐的npm包,92%是编出来的
前端·javascript·ai编程
zephyr055 小时前
动态规划-最长上升子序列问题
算法·动态规划
心中有国也有家6 小时前
AtomGit Flutter 鸿蒙客户端:Canvas 绘制进阶-路径、渐变与混合模式
android·javascript·flutter·华为·harmonyos