引言
大家好!今天我们来聊聊 LeetCode 上一道非常经典的算法题 —— 三数之和(3Sum) 。你可能听说过它,也可能被它“折磨”过。别担心,今天我会用最通俗易懂的方式,带你一步步理解这道题的核心思想,还会手把手教你如何写出高效的解法!
更重要的是,我们还会一起看看 JavaScript 中一个神奇又常用的方法:nums.sort()。别小看它,它可是我们解决三数之和问题的关键第一步!
题目长啥样?
题目大概是这样的:
给你一个整数数组
nums,请你找出所有 不重复 的三元组[a, b, c],使得a + b + c = 0。
比如:
输入:nums = [-1, 0, 1, 2, -1, -4]
输出:[[-1, -1, 2], [-1, 0, 1]]
注意:不能有重复的三元组!顺序不同但数字一样的也算重复哦。
暴力解法?太慢了!
最容易想到的办法就是“暴力三重循环”:遍历所有可能的三个数,看看加起来是不是 0。
时间复杂度是 O(n³) —— 如果数组有 1000 个数,那就要算 10 亿次!电脑会累趴下 😫。
所以我们需要更聪明的办法!
聪明人的思路:排序 + 双指针
第一步:先排序!
为什么排序这么重要?因为有序的数组能让我们用“双指针”技巧快速缩小搜索范围。
来看这段代码:
const nums = [2, 1, 6, 3, 4, 5];
// b在前面,a在后面 a-b<0 交换位置 升序
nums.sort((a, b) => {
console.log(a , b); // 打印结果为:
// 1 2
// 6 1
// 6 2
// 3 2
// 3 6
// 4 3
// 4 6
// 5 3
// 5 6
// 5 4
return a - b;
});
console.log(nums); // [1, 2, 3, 4, 5, 6]
这段代码干了啥?
- 它调用了
nums.sort((a, b) => a - b)。 - 这个写法的意思是:按从小到大排序(升序) 。
- 当
a - b < 0时,说明a比b小,不需要交换 → 正确顺序。 - JavaScript 的
sort方法内部其实不是冒泡排序(实际是 Timsort 或快排变种),但我们可以用冒泡的思想来理解:它会不断比较两个数,决定谁放前面。
最终,[2, 1, 6, 3, 4, 5] 变成了 [1, 2, 3, 4, 5, 6] —— 整整齐齐!
🔍 小知识:如果不传参数,
sort()会把数字当字符串排!比如[10, 2]会变成[10, 2](因为 "1" < "2")。所以一定要写(a, b) => a - b!
核心算法:固定一个数 + 双指针找另外两个
现在数组有序了,我们就可以用这个绝妙策略:
-
固定第一个数(比如
nums[i])。 -
在它右边的子数组中,用左指针(left)和右指针(right) 找另外两个数。
left = i + 1right = nums.length - 1
-
计算三数之和:
- 如果 等于 0 → 找到答案!加入结果,并移动双指针继续找。
- 如果 小于 0 → 说明太小了,把
left右移(拿更大的数)。 - 如果 大于 0 → 说明太大了,把
right左移(拿更小的数)。
来看完整代码:
function threeSum(nums) {
// 先排序
// sort js 内置的排序 冒泡的思想理解
// a - b < 0 a 小 b 大 不交换位置 升序
// b - a < 0 b 小 a 大 交换位置 降序
nums.sort((a, b) => a - b);
const res = [];
// 固定一个数字
// 连个指针 i +1 j = nums.length-1
for (let i = 0; i < nums.length-2; i++) {
// 跳过重复的起点
// i == 0 时 是第一个数字 不需要跳过
// i j k 三个数字不能重复
if (i > 0 && nums[i] === nums[i-1]) continue;
// 双指针
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) {
left++;
} else {
right--;
}
}
}
return res;
}
关键细节:去重!
你可能会问:“怎么保证结果不重复?”
看这两行:
if (i > 0 && nums[i] === nums[i-1]) continue;
→ 如果当前固定的数和上一个一样,直接跳过!避免重复三元组。
还有找到答案后:
while(left < right && nums[left] === nums[left-1]) left++;
while(left < right && nums[right] === nums[right+1]) right--;
→ 把左右指针也跳过重复值,确保下一组是全新的组合!
时间复杂度分析
- 排序:O(n log n)
- 外层循环:O(n)
- 内层双指针:O(n)
- 总体:O(n²) —— 比暴力快太多了!
总结:三步搞定三数之和
- 排序:让数组变得“听话”,方便我们控制方向。
- 固定 + 双指针:像夹心饼干一样,从两边向中间逼近目标。
- 跳过重复:确保答案干净、不冗余。
这道题不仅考察你的编码能力,更考验你对“有序性”和“指针移动”的理解。一旦掌握,你会发现很多类似题目(比如四数之和、最接近的三数之和)都能迎刃而解!
希望这篇博客让你觉得算法不再可怕,反而有点酷!如果你以前看到 sort 就头疼,现在应该能自信地说:“哦,我知道它在干嘛!”
下次见,继续一起征服算法世界!🚀