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)。

最后唠两句

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

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

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

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

相关推荐
郑州光合科技余经理3 小时前
代驾系统架构拆解:订单链路、权限组织与私有化源码交付
开发语言·后端·算法·架构·系统架构·uni-app·php
To_OC5 小时前
LC 35 搜索插入位置:二分查找谁都会,边界条件谁写谁懵
javascript·算法·leetcode
码哥DFS6 小时前
二叉树的直径
开发语言·javascript·算法
光影少年8 小时前
RN的Fabric 渲染流程
运维·前端·javascript·react native·react.js·fabric
营养充电站9 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
人工智能·算法·docker·jupyter
技术小黑9 小时前
RNN算法实战系列05 | 天气预测
人工智能·rnn·算法
用户9385156350710 小时前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
kyriewen10 小时前
我重写了自己用了两年的防抖节流Hook——发现里面藏着3个隐藏bug
前端·javascript·面试
漂流瓶jz10 小时前
Webpack开发环境:观察模式/webpack-dev-server/HMR热更新
前端·javascript·webpack