【LeetCode Hot100刷题笔记】:三数之和—— 掌握双指针与排序去重的核心思想
题目来源:第 15 题(3Sum)
难度:Easy → Medium
标签:数组、哈希表、双指针、排序
Hot 100 必刷指数:⭐⭐⭐⭐⭐
在 LeetCode Hot 100 的高频题中,「两数之和」 是几乎所有初学者的第一道题,而 「三数之和」 则是进阶路上的“分水岭”。它们看似独立,实则一脉相承——前者教你用空间换时间,后者教你用排序+双指针解决更复杂的组合问题。
本文将带你:
- 回顾「两数之和」的经典解法;
- 分析为何不能直接套用哈希表解决「三数之和」;
- 深入讲解 JavaScript
sort()的正确用法(这是很多人的盲区!); - 手把手实现高效、无重复的「三数之和」解法。
🔹 1. 两数之和(Two Sum)—— 哈希表的胜利
📌 题目简述
给定数组 nums 和目标值 target,返回两个数的下标,使它们的和等于 target。
❌ 不好的方法:暴力破解法(O(n^2))
- 暴力解法需要对每个元素
nums[i],遍历其后所有元素nums[j](j > i),检查是否满足nums[i] + nums[j] == target。 - 对于长度为
n的数组,最坏情况下要进行约n(n−1)/2次比较。 - 当
n较大时(例如n = 10⁵),操作次数将高达 50 亿次,在大多数编程环境中会超时(LeetCode 通常限制 1~2 秒运行时间)。
✅ 最优解:哈希表(O(n))
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const need = target - nums[i];
if (map.has(need)) return [map.get(need), i];
map.set(nums[i], i);
}
}
- 核心思想:把“找两个数”转化为“对每个数,看它的补数是否出现过”。将求和问题变成了求差
- 无需排序,因为题目要求返回原始下标,排序会打乱位置。
💡 小结:两数之和 → 哈希表 + 求差
🔸 2. 三数之和(3Sum)—— 为什么哈希不够用了?
❌ 能否用哈希?
理论上可以:固定 a,然后在剩余数组中用哈希找 b + c = -a。
但问题在于:如何高效去重?
例如输入 [-1, 0, 1, 2, -1, -4],若不排序,你很难判断 [-1, 0, 1] 和 [0, -1, 1] 是同一组解。
✅ 结论:当问题涉及“组合去重”时,排序是天然的去重工具。
🔧 3. 关键前置:JavaScript sort() 的正确打开方式
很多同学在写 3Sum 时,直接写 nums.sort(),结果在负数测试用例上翻车!
❌ 错误示范
let arr = [-1, -2, 3, 10];
arr.sort();
console.log(arr); // [-1, -2, 10, 3] ❌
原因:sort() 默认按字符串 Unicode 编码比较,"-1" 字符串小于 "-2"。
✅ 正确写法:传入比较函数
nums.sort((a, b) => a - b); // 升序
📚 比较函数逻辑解析:
-
(a, b) => a - b- 若
a < b→ 返回负数 →a排在b前 → 升序 - 若
a > b→ 返回正数 →b排在a前
- 若
-
这不是冒泡排序!现代 JS 引擎(如 V8)使用 Timsort(混合稳定排序),效率远高于 O(n²)
💡 记住口诀: “a - b 升序,b - a 降序”
🔥 4. 三数之和:排序 + 双指针 + 去重
🧠 解题思路
-
排序:
O(n log n),为双指针和去重奠基; -
固定第一个数
nums[i]:- 若
nums[i] > 0,直接 break(后面都是正数,和不可能为 0); - 若
i > 0 && nums[i] === nums[i-1],跳过(避免重复三元组);
- 若
-
双指针查找:
left = i + 1,right = n - 1- 根据
sum = nums[i] + nums[left] + nums[right]调整指针;
-
找到解后继续去重:移动指针并跳过相同值。
💻 完整代码(带详细注释)
var threeSum = function(nums) {
// Step 1: 正确升序排序
nums.sort((a, b) => a - b);
const res = [];
// Step 2: 固定第一个数
for (let i = 0; i < nums.length - 2; i++) {
// 剪枝:最小值已大于0,不可能和为0
if (nums[i] > 0) break;
// 跳过重复的起始值
if (i > 0 && nums[i] === nums[i - 1]) continue;
// Step 3: 双指针
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
res.push([nums[i], nums[left], nums[right]]);
// 移动指针
left++;
right--;
// 关键:跳过重复值,避免重复三元组
while (left < right && nums[left] === nums[left - 1]) left++;
while (left < right && nums[right] === nums[right + 1]) right--;
}
else if (sum > 0) {
right--; // 和太大,右指针左移
}
else {
left++; // 和太小,左指针右移
}
}
}
return res;
};
🧪 测试用例
console.log(threeSum([-1,0,1,2,-1,-4]));
// [[-1,-1,2],[-1,0,1]]
console.log(threeSum([0,0,0]));
// [[0,0,0]]
console.log(threeSum([1,2,-2,-1]));
// []
⏱️ 复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 暴力三重循环 | O(n³) | O(1) |
| 排序 + 双指针 | O(n²) | O(1) 或 O(log n)(排序栈空间) |
🧠 思维升华:从 2Sum 到 3Sum 的范式迁移
| 维度 | 两数之和 | 三数之和 |
|---|---|---|
| 目标 | 找下标 | 找值(且去重) |
| 核心数据结构 | 哈希表 | 排序数组 |
| 关键技巧 | “求差” | “双指针收缩” |
| 是否需要排序 | ❌ 否 | ✅ 是 |
| 去重策略 | 无(下标天然唯一) | 主动跳过重复元素 |
🌟 启示:
- 当问题从“找下标”变为“找组合”,且要求无重复时,排序 + 双指针 往往比哈希更优雅。
- 这一思想可扩展至 4Sum、最接近的三数之和、三数之和小于目标值 等 Hot 100 高频题。
✅ 总结
- 两数之和:哈希表是王者,O(n) 解决问题;
- 三数之和:排序是基石,双指针是利剑,去重是灵魂;
- JavaScript 开发者注意:
sort()必须传入(a, b) => a - b才能正确排序数字!
掌握这两道题,你就掌握了 LeetCode 数组类问题的两大核心范式。继续刷下去,Hot 100 不再可怕!
📌 延伸练习:
- LeetCode 18. 四数之和(4Sum)
- LeetCode 16. 最接近的三数之和
- LeetCode 259. 较小的三数之和(3Sum Smaller)
刷题不在多,而在精。理解一道题,胜过盲目十道。