三数之和:从暴力破解到优雅双指针
作为一个算法爱好者,我曾经在深夜的咖啡馆里,面对三数之和问题,差点把咖啡洒在键盘上。但经过一番思考,我发现排序+双指针的组合,就像给算法装上了火箭推进器,瞬间起飞!今天,让我们一起探索这个经典算法题,看看如何从暴力破解的泥潭中跳出来,走向优雅的双指针世界。
一、暴力破解:效率低到让人想哭
想象一下,你有1000个数字,要找出所有三个数之和等于目标值的组合。最直接的思路是用三个嵌套循环:
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) {
for (let k = j + 1; k < nums.length; k++) {
if (nums[i] + nums[j] + nums[k] === target) {
// 找到结果
}
}
}
}
这种暴力方法的时间复杂度是 O(n³) 。对于1000个数字,我们需要尝试大约 166,167,000 次操作(C(1000,3))。
效率有多低? 让我们用一个生动的比喻:
你有1000个朋友,想找出所有生日加起来是1000的三人组。如果用暴力方法,你得尝试 C(1000,3) = 166,167,000 种组合。这相当于在1000个朋友中,尝试所有可能的三人组,直到找到满足条件的组合。你可能会想:"这比我在餐厅点餐还要麻烦!"
更糟糕的是,如果输入规模是10,000,暴力方法需要尝试约 166,670,000,000 次操作。即使你的电脑每秒能处理10亿次操作,也需要166秒。而如果输入是100万,那需要 166,666,666,666,666 次操作,相当于 5270年!这可不是我们想要的算法。
二、优化思路:先排序,再用双指针
核心思想: 先对数组排序,然后固定一个数字,用两个指针分别从两端向中间移动,高效地查找满足条件的组合。
为什么这个方法有效?因为排序后,我们可以利用数组的有序性,通过移动指针来高效地找到满足条件的组合。
为什么排序能提高效率?
排序后,数组是升序排列的。固定一个数字后,我们可以:
- 从固定数字的下一个位置开始,用
left指针 - 从数组末尾开始,用
right指针 - 根据当前和与目标值的大小关系,调整指针位置
关键点:
- 如果当前和
sum > 0,说明需要减小和,right指针左移 - 如果当前和
sum < 0,说明需要增大和,left指针右移 - 如果当前和
sum = 0,找到一组解,加入结果集,并跳过重复数字
代码实现详解
function threeSum(nums) {
// 先排序,时间复杂度 O(nlogn)
nums.sort((a, b) => a - b);
const res = [];
for (let i = 0; i < nums.length - 2; i++) {
// 跳过重复的数字
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]]);
// 跳过重复的数字
while (left < right && nums[left] === nums[left + 1]) {
left++;
}
while (left < right && nums[right] === nums[right - 1]) {
right--;
}
left++;
right--;
} else if (sum > 0) {
right--;
} else {
left++;
}
}
}
return res;
}
执行过程演示:
假设输入 nums = [2, 1, 6, 3, 4, 5],目标值为 0。
-
先排序:
[1, 2, 3, 4, 5, 6] -
固定第一个数字
1,left指向2,right指向61 + 2 + 6 = 9 > 0→right左移1 + 2 + 5 = 8 > 0→right左移- ...
1 + 2 + 3 = 6 > 0→right左移- 没有找到和为 0 的组合
-
固定
2,left指向3,right指向62 + 3 + 6 = 11 > 0→right左移2 + 3 + 5 = 10 > 0→right左移- ...
2 + 3 + 4 = 9 > 0→right左移- 没有找到和为 0 的组合
-
固定
3,left指向4,right指向63 + 4 + 6 = 13 > 0→right左移3 + 4 + 5 = 12 > 0→right左移- 没有找到和为 0 的组合
-
固定
4,left指向5,right指向64 + 5 + 6 = 15 > 0→right左移- 没有找到和为 0 的组合
最终,对于这个输入,没有三数之和为 0 的组合。
三、sort方法:排序的艺术
在JavaScript中,sort() 方法用于对数组进行排序。默认情况下,sort() 方法会将元素转换为字符串,然后按字符串的 Unicode 顺序进行排序。
const nums = [2, 1, 6, 3, 4, 5];
nums.sort((a, b) => { console.log(a, b); return a - b; });
比较函数解释:
a - b < 0:a应该排在b前面(升序)a - b > 0:a应该排在b后面(降序)a - b = 0:a和b顺序不变
在我们的三数之和问题中,我们使用 nums.sort((a, b) => a - b) 来对数组进行升序排序,这样我们可以方便地使用双指针。
排序的效率:
- JavaScript 的
sort()方法通常使用快速排序或归并排序,时间复杂度为 O(nlogn) - 这比暴力方法的 O(n³) 低得多,是优化的关键
四、为什么这个方法优雅?
- 效率高: 时间复杂度为 O(n²) (排序 O(nlogn) + 双指针 O(n²))
- 避免重复: 通过排序和指针移动,自然避免了重复的三元组
- 空间效率好: 只需要常数空间来存储指针和结果
优化对比:
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力破解 | O(n³) | O(1) | 小规模输入 |
| 排序+双指针 | O(n²) | O(1) | 大规模输入 |
五、幽默小结
三数之和问题告诉我们:排序不是为了好看,而是为了效率。 就像在超市购物,如果你先按商品类别排序,找东西会快很多,而不是在货架上乱翻。
有一次,我问一个朋友:"你为什么总是用暴力方法解题?" 他回答:"因为我想试试运气。" 我说:"那下次试试排序+双指针吧,保证让你的代码跑得比你的咖啡还快!"
三数之和问题的优雅之处,在于它教会我们:有时候,把问题'排序'一下,答案就自然浮现了。 这就像把散落的乐高积木按颜色和形状分类,很快就能找到匹配的组合。
六、实际应用
三数之和问题在现实世界中有许多应用,例如:
- 金融领域: 找出三个股票的组合,使得它们的总回报率等于目标值
- 推荐系统: 找出三个商品的组合,使得它们的推荐度之和达到某个阈值
- 生物信息学: 找出三个基因的组合,使得它们的表达量之和满足某种条件
七、结语
三数之和问题是一个经典的算法题,它告诉我们:在面对复杂问题时,不要急于动手,先想一想有没有更优雅的解决方案。
排序+双指针的思路,就像给算法装上了火箭推进器,从 O(n³) 的泥潭中跳出来,飞向 O(n²) 的高效世界。
下次当你面对一个看起来很复杂的问题时,先想想:能不能先排序?也许你会发现一个更优雅的解决方案。
记住:在算法的世界里,排序常常是解决复杂问题的钥匙,而双指针则是打开这把钥匙的完美工具。
现在,去试试看吧!也许你的下一个算法题,会因为一个简单的排序,而变得无比优雅!