三数之和的优化艺术:从暴力到双指针的蜕变之路
如何优雅地解决“三数之和”这个经典算法题?今天我们将深入探讨双指针解法的精妙之处,并分享一些实用的优化技巧。
一、问题重述:三数之和
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a、b、c,使得 a + b + c = 0?找出所有满足条件且不重复的三元组。
示例:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
二、解题思路演进
1. 暴力解法(三重循环)❌
// 时间复杂度 O(n³),不可接受
for(let i=0; i a - b);
let ans = [];
for(let i = 0; i < nums.length - 2; i++){
let min = nums[i];
let l = i + 1;
let r = nums.length - 1;
// 2. 跳过重复的起始元素
if(i > 0 && min == nums[i - 1]){
continue;
}
// 3. 剪枝优化
if(min + nums[i+1] + nums[i+2] > 0){
break; // 最小组合都大于0,后面的更大
}
if(min + nums[r] + nums[r-1] < 0){
continue; // 最大组合都小于0,当前min太小
}
// 4. 双指针寻找另外两个数
while(l < r){
let sum = nums[l] + nums[r] + min;
if(sum > 0){
r--;
} else if(sum < 0){
l++;
} else {
// 找到解
ans.push([min, nums[l], nums[r]]);
l++;
r--;
// 5. 跳过重复元素
while(l < r && nums[l] === nums[l-1]) l++;
while(l < r && nums[r] === nums[r+1]) r--;
}
}
}
return ans;
};
四、关键技巧详解
技巧1:排序是双指针的基础
nums.sort((a, b) => a - b);
// 排序后:[-4, -1, -1, 0, 1, 2]
为什么需要排序?
- 方便去重
- 双指针移动有依据(和大了右指针左移,和小了左指针右移)
- 便于剪枝优化
技巧2:三重去重策略
去重是本题最大的难点!
// 1. 外层循环去重
if(i > 0 && nums[i] === nums[i-1]) continue;
// 2. 找到解后左指针去重
while(l < r && nums[l] === nums[l-1]) l++;
// 3. 找到解后右指针去重
while(l < r && nums[r] === nums[r+1]) r--;
技巧3:智能剪枝优化
这两处剪枝能大幅提升性能:
// 剪枝1:当前最小组合已经>0,后面不可能有解
// 示例:nums[i] = 1, nums[i+1] = 2, nums[i+2] = 3
// 1+2+3 = 6 > 0,后面的数字更大,直接break
if(min + nums[i+1] + nums[i+2] > 0){
break;
}
// 剪枝2:当前最大组合仍然 0){
r--; // 和太大,需要减小
} else if(sum < 0){
l++; // 和太小,需要增大
} else {
// 找到解
// 注意:找到解后两个指针都要移动!
l++;
r--;
}
}
五、算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 排序 | O(n log n) | O(1) |
| 双指针遍历 | O(n²) | O(1) |
| 总体 | O(n²) | O(1) |
优化效果:
- 原始暴力解法:O(n³)
- 优化后:O(n²)
- 在 n=3000 时,速度提升约 1000 倍!
六、实际测试与验证
// 测试用例
const testCases = [
[-1, 0, 1, 2, -1, -4],
[0, 0, 0, 0],
[-2, 0, 1, 1, 2],
[],
[0, 1, 1]
];
testCases.forEach((nums, idx) => {
console.log(`测试用例 ${idx + 1}:`, nums);
console.log('结果:', threeSum(nums));
console.log('---');
});
七、常见错误与陷阱
错误1:忘记排序
// ❌ 错误:未排序直接使用双指针
// ✅ 正确:必须先排序
错误2:去重逻辑不完整
// ❌ 只在外层去重,内层不去重
// 会导致 [[-1,-1,2],[-1,0,1],[-1,0,1]] 这样的重复结果
错误3:指针移动时机错误
// ❌ 找到解后只移动一个指针
// ✅ 必须同时移动两个指针
八、举一反三:四数之和
掌握了三数之和,四数之和就迎刃而解:
var fourSum = function(nums, target) {
nums.sort((a, b) => a - b);
let ans = [];
for(let i = 0; i < nums.length - 3; i++){
// 去重
if(i > 0 && nums[i] === nums[i-1]) continue;
for(let j = i + 1; j < nums.length - 2; j++){
// 去重
if(j > i + 1 && nums[j] === nums[j-1]) continue;
let l = j + 1, r = nums.length - 1;
while(l < r){
const sum = nums[i] + nums[j] + nums[l] + nums[r];
if(sum > target){
r--;
} else if(sum < target){
l++;
} else {
ans.push([nums[i], nums[j], nums[l], nums[r]]);
l++; r--;
// 去重
while(l < r && nums[l] === nums[l-1]) l++;
while(l < r && nums[r] === nums[r+1]) r--;
}
}
}
}
return ans;
};
九、总结与启示
核心思想:
- 排序是前提 - 为双指针和去重奠定基础
- 双指针是核心 - 将 O(n³) 降为 O(n²)
- 去重是关键 - 多层级去重保证结果唯一性
- 剪枝是优化 - 提前终止不可能的分支
算法哲学:
- 好的算法不仅是解决问题,更是优雅地解决问题
- 空间换时间,时间换空间,找到平衡点
- 边界条件处理是区分普通和优秀程序员的标志
思考题:
- 如果要求返回三元组的索引而不是值,如何修改?
- 如果数组非常大(n>10^5),内存有限怎么办?
- 如何扩展到 k 数之和问题?
算法之美,在于其简洁与高效。 三数之和问题完美展示了如何通过巧妙的思路,将复杂问题简单化。掌握这种思维模式,你就能解决一大类"多个数之和"的问题了!
互动问题: 你还能想到哪些优化三数之和的方法?欢迎在评论区分享你的想法!