引言
在 LeetCode 的浩瀚题海中,《三数之和》(Three Sum) 15. 三数之和 - 力扣(LeetCode) 是一道看似平凡却极具教学价值的经典题。它不考复杂数据结构,也不依赖高深数学,却精准地考察了你对问题转化、时间复杂度优化、边界控制与去重逻辑的综合掌控能力。
本文将带你完成一次完整的思维跃迁:从最朴素的暴力解法出发,逐步拆解瓶颈,最终抵达那个优雅高效的 O(n²) 解法——并在此过程中,揭示“排序 + 双指针”这一组合为何能成为解决多数之和类问题的黄金范式。
一、暴力解法:为什么它注定失败?
最直觉的思路是三层嵌套循环:
for (i) for (j > i) for (k > j) if (nums[i] + nums[j] + nums[k] === 0) ...
- 时间复杂度:
O(n³)—— 当n = 1000时,操作次数高达 10⁹,远超 1 秒时限。 - 重复问题:即使找到所有三元组,还需用
Set或哈希表去重,空间开销大,代码臃肿。 - 根本缺陷:暴力法没有利用任何数组结构信息,纯粹穷举,效率低下。
💡 关键洞察:当问题涉及“多个元素组合满足某种和条件”时,若能固定部分变量,往往可转化为更简单的子问题。
二、核心思想:降维打击 —— 从“三数之和”到“两数之和”
我们重新审视目标:
寻找 a + b + c = 0 ⇨ 等价于 b + c = -a。
如果我们先固定 a,问题就变成了:在剩余数组中找两个数,使其和为 -a —— 这正是经典的「两数之和」问题!
但注意:普通的「两数之和」可用哈希表 O(n) 解决,但本题要求返回所有不重复三元组,且不能使用额外索引映射(因元素可重复)。此时,排序 + 双指针 成为最优选择。
三、为什么排序是破局关键?
对数组排序后,带来两大优势:
- 天然支持去重:相同值连续排列,跳过重复只需比较相邻元素。
- 支持有序搜索:双指针可根据当前和的大小,确定性地移动以逼近目标。
🌰 举例:
[-1, 0, 1, 2, -1, -4]→ 排序后[-4, -1, -1, 0, 1, 2]
此时,若固定a = -1,只需在[ -1, 0, 1, 2 ]中找和为1的两个数。
四、双指针:如何高效搜索“两数之和”?
在已排序的子数组中:
-
左指针
L = i+1,右指针R = n-1 -
计算
sum = a + nums[L] + nums[R]- 若
sum == 0→ 找到答案,记录并同时移动双指针 - 若
sum < 0→ 和太小,需增大 →L++ - 若
sum > 0→ 和太大,需减小 →R--
- 若
✅ 为什么能保证不漏解?
因为数组有序,对于固定的a,所有可能的(b, c)组合构成一个单调二维平面。双指针相当于从右上角开始“爬山”,每一步都排除一行或一列,覆盖全部可能性。
五、去重:三重防线,杜绝重复三元组
这是本题最容易出错的地方。必须在三个层级进行去重:
1️⃣ 基准数去重(外层循环)
if (i > 0 && nums[i] === nums[i - 1]) continue;
避免以相同值作为
a重复启动搜索。
2️⃣ 左指针去重(找到解后)
while (L < R && nums[L] === nums[L + 1]) L++;
3️⃣ 右指针去重(找到解后)
while (L < R && nums[R] === nums[R - 1]) R--;
⚠️ 注意:去重要在记录结果之后进行!否则会跳过有效解。
六、完整代码实现(JavaScript)
var threeSum = function(nums) {
const res = [];
const n = nums.length;
if (n < 3) return res;
nums.sort((a, b) => a - b); // 关键第一步:排序
for (let i = 0; i < n - 2; i++) {
// 优化:最小值 > 0,后续不可能和为0
if (nums[i] > 0) break;
// 基准数去重
if (i > 0 && nums[i] === nums[i - 1]) continue;
let L = i + 1, R = n - 1;
while (L < R) {
const sum = nums[i] + nums[L] + nums[R];
if (sum === 0) {
res.push([nums[i], nums[L], nums[R]]);
// 左右指针去重
while (L < R && nums[L] === nums[L + 1]) L++;
while (L < R && nums[R] === nums[R - 1]) R--;
L++;
R--;
} else if (sum < 0) {
L++;
} else {
R--;
}
}
}
return res;
};
七、复杂度分析
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间 | O(n²) | 排序 O(n log n) + 外层 O(n) × 内层双指针 O(n) |
| 空间 | O(log n) ~ O(n) | 取决于排序算法(V8 引擎通常为快排,栈空间 O(log n)) |
📌 对比暴力法:从
10⁹操作降至10⁶,提升千倍以上!
八、示例走查:眼见为实
输入:[-1, 0, 1, 2, -1, -4]
排序后:[-4, -1, -1, 0, 1, 2]
-
i=0(-4):无解(最大和 = -4+1+2 = -1 < 0) -
i=1(-1):L=2(-1), R=5(2)→ sum=0 →[-1,-1,2]- 去重后
L=3(0), R=4(1)→ sum=0 →[-1,0,1]
-
i=2(-1):与前一个相同,跳过 -
i=3(0):0 > 0不成立,但后续0+1+2=3>0,无解 -
i>=3后nums[i] >= 0,且i=3时已无法构成和为0,提前终止
✅ 输出:[[-1,-1,2], [-1,0,1]]
九、常见陷阱 & 工程建议
- 忘记边界检查:
n < 3直接返回空。 - 去重顺序错误:必须先
push再去重,否则漏解。 - 未利用排序性质提前终止:当
nums[i] > 0时,后续全为正,和不可能为0。 - 指针越界:所有
while去重必须包含L < R条件。
🔧 工程思维延伸:该模式可无缝扩展至「四数之和」——固定两个数,内层双指针;甚至「K 数之和」可用递归 + 双指针通解。
十、总结:不止于一道题
《三数之和》的价值,远不止于通过一次面试。它教会我们:
- 问题转化:复杂问题 → 熟悉子问题(三数 → 两数)
- 结构利用:排序不是目的,而是为高效搜索和去重服务
- 指针艺术:双指针不仅是技巧,更是对“有序性”的极致利用
- 边界意识:算法正确性 = 逻辑 + 边界 + 去重
当你下次面对“多数之和”、“窗口匹配”、“有序数组查找”等问题时,不妨回想这个经典模型——排序 + 双指针 + 三重去重,或许就是那把打开新世界的钥匙。
📢 互动时间:你在刷这道题时踩过哪些坑?是否尝试过其他解法(如哈希表)?欢迎在评论区分享你的思考!
👍 如果觉得有收获,别忘了点赞 + 收藏 + 转发,让更多人看到这份清晰的解题指南!