🧮 在算法与数据结构的学习中,「三数之和」(3Sum)是一个经典且极具教学意义的问题。它不仅考察对数组操作的熟练度,还涉及排序、去重、双指针等核心技巧。本文将深入剖析该问题,并结合代码实现、时间复杂度分析以及相关知识扩展,帮助读者全面掌握这一题型。
🔍 问题描述
给定一个整数数组 nums,判断是否存在三个不同的元素 a、b、c,使得 a + b + c = 0。要求返回所有满足条件的不重复三元组。
注意:答案中不可以包含重复的三元组。例如
[−1, 0, 1]和[0, −1, 1]被视为相同,只应出现一次。
LeetCode 题目链接:leetcode.cn/problems/3s…
⚡ 暴力解法(O(n³))
最直观的思路是三重循环枚举所有可能的三元组:
for (let i = 0; i < n; i++) {
for (let j = i + 1; j < n; j++) {
for (let k = j + 1; k < n; k++) {
if (nums[i] + nums[j] + nums[k] === 0) {
// 收集结果(需去重)
}
}
}
}
- 时间复杂度:O(n³)
- 空间复杂度:O(1)(忽略结果存储)
但此方法在 n 较大时(如 n = 1000)会超时,且去重逻辑复杂——需要对每个三元组排序后用 Set 去重,效率低下。
🔄 优化思路:排序 + 双指针(O(n²))
✅ 核心思想
-
先对数组排序(升序)
- 排序后便于跳过重复元素。
- 为双指针提供有序性基础。
-
固定第一个数
nums[i]- 遍历
i从0到n - 3。 - 若
nums[i] > 0,由于数组已排序,后续所有数都 ≥nums[i],三数之和不可能为 0,可提前终止。
- 遍历
-
使用双指针在剩余区间
[i+1, n-1]中寻找两数之和为-nums[i]-
左指针
left = i + 1 -
右指针
right = n - 1 -
计算
sum = nums[i] + nums[left] + nums[right]- 若
sum === 0→ 找到一组解,加入结果,并移动双指针继续搜索。 - 若
sum < 0→ 左指针右移(增大和) - 若
sum > 0→ 右指针左移(减小和)
- 若
-
-
关键:跳过重复元素
- 固定
i时,若nums[i] === nums[i-1],跳过(避免重复三元组)。 - 找到解后,
left和right移动时也要跳过重复值。
- 固定
💻 完整代码实现(JavaScript)
function threeSum(nums) {
// 先排序(快排 O(n log n))
// JavaScript 的 sort 默认使用 Timsort(混合排序),但比较函数决定顺序
nums.sort((a, b) => a - b);
const res = [];
// 固定第一个数字 i
for (let i = 0; i < nums.length - 2; i++) {
// 跳过重复的起点
// i === 0 时是第一个数字,不需要跳过
if (i > 0 && nums[i] === nums[i - 1]) continue;
// 若当前最小值 > 0,三数之和必 > 0,提前结束
if (nums[i] > 0) break;
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;
}
📊 时间与空间复杂度分析
- 排序:O(n log n)
- 外层循环:O(n)
- 内层双指针:O(n)
- 总时间复杂度:O(n log n) + O(n²) = O(n²)
- 空间复杂度:O(1)(不计结果数组)
远优于暴力 O(n³),适用于 n ≤ 3000 的典型 LeetCode 输入规模。
🧠 关于排序的细节补充
在 JavaScript 中,Array.prototype.sort() 默认按字符串 Unicode 码排序,因此必须传入比较函数:
nums.sort((a, b) => a - b); // 升序
- 当
a - b < 0时,a排在b前面 → 升序。 - 内部实现并非冒泡排序(尽管教学时常以冒泡类比),现代引擎多采用 Timsort(Python 启发的混合稳定排序),兼具快排与归并的优点。
示例(来自 2.js):
const nums = [2, 1, 6, 3, 4, 5];
nums.sort((a, b) => {
console.log(a, b); // 展示比较过程(实际调用顺序依赖内部算法)
return a - b;
});
console.log(nums); // 输出 [1, 2, 3, 4, 5, 6]
🔁 与「两数之和」的对比
| 特性 | 两数之和(Two Sum) | 三数之和(Three Sum) |
|---|---|---|
| 目标 | 找两个数和为 target | 找三个数和为 0 |
| 是否可哈希 | ✅ 可用 HashMap O(n) | ❌ 哈希难以处理三元组去重 |
| 是否需排序 | 否(若只需返回索引) | ✅ 必须排序以支持双指针和去重 |
| 去重要求 | 通常无(索引唯一) | ✅ 必须去重三元组 |
| 典型解法 | HashMap | 排序 + 双指针 |
💡 三数之和无法直接用 HashMap 高效解决,因为需要确保三元组不重复,而哈希表难以维护组合的唯一性。
🛑 常见陷阱与注意事项
-
越界问题
- 外层循环
i < nums.length - 2,确保left和right有空间。
- 外层循环
-
重复跳过时机
i > 0 && nums[i] === nums[i-1]→ 在进入内层前跳过。- 找到解后,先移动指针再跳过重复,避免漏解。
-
提前终止条件
if (nums[i] > 0) break;是重要优化,但仅在目标为 0 时成立。
-
空输入处理
- 若
nums.length < 3,直接返回空数组。
- 若
🌟 扩展思考
- 四数之和(4Sum) ?
可在三数之和基础上再套一层循环,时间复杂度 O(n³),同样使用排序+双指针+去重。 - 通用 kSum 问题?
可用递归将 kSum 降维至 2Sum,时间复杂度 O(n^{k-1})。 - 若目标不是 0 而是任意 target?
只需将判断条件改为sum === target,其余逻辑不变。
✅ 总结
「三数之和」是一道集排序、双指针、去重、边界处理于一体的综合题。通过将暴力 O(n³) 优化至 O(n²),不仅提升了效率,更展示了算法设计中“利用有序性降低复杂度”的核心思想。掌握此题,对理解后续的 kSum 系列问题、滑动窗口、双指针技巧都有极大帮助。
🎯 记住口诀:
一排序,二固定,三双指,四去重,五剪枝。
现在,你已经准备好征服所有类似的求和问题了!🚀