三数之和(3Sum)详解:从暴力到双指针优化
在算法面试与实际开发中, “三数之和” 是一个经典问题,它不仅考察对数组操作的理解,更深入检验对时间复杂度优化、去重逻辑以及双指针技巧的掌握。本文将结合你的笔记与代码,系统梳理 三数之和问题的完整解法演进路径,突出核心思想与实现细节。
🧩 问题描述
给定一个整数数组 nums,找出所有不重复的三元组 [a, b, c],使得:
注意:结果中不能包含重复的三元组。
示例:
输入:nums = [-1, 0, 1, 2, -1, -4]
输出:[[-1, -1, 2], [-1, 0, 1]]
⏳ 解法演进:从暴力到高效
1️⃣ 暴力解法(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(n²),但可用哈希表优化到 O(n)。三数之和能否也优化?
2️⃣ 优化思路:排序 + 双指针(O(n²))
✅ 核心思想
- 先排序:使数组有序 → 利用单调性控制指针移动方向。
- 固定一个数
nums[i],转化为“在子数组中找两数之和等于-nums[i]”的问题。 - 双指针扫描:左指针
left = i+1,右指针right = n-1,根据当前和调整指针。
这本质上是将 三数之和 转化为多次 两数之和(有序数组版) 。
🔑 为什么排序能帮助去重?
排序后,相同元素会相邻。因此:
- 固定
i时,若nums[i] == nums[i-1],说明该值已处理过,直接跳过。 - 找到一组解后,移动
left和right时,也要跳过重复值。
🧠 算法步骤详解
以你的代码为基础,拆解关键步骤:
function threeSum(nums) {
// 1. 边界处理(可选)
if (nums.length < 3) return [];
// 2. 升序排序
nums.sort((a, b) => a - b); // 注意:JS sort 默认按字符串排序!
const res = [];
// 3. 外层循环:固定第一个数 nums[i]
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;
// 4. 双指针查找
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;
}
🔍 关键点解析
✅ 排序的作用
- 启用双指针:只有有序数组才能通过比较和来决定指针移动方向。
- 简化去重:重复元素聚集,只需比较相邻项。
📌 JS 的
Array.prototype.sort()默认按字符串 Unicode 排序!
必须传入(a, b) => a - b才能得到正确的数值升序。
const nums = [2, 1, 6, 3, 4, 5];
nums.sort((a, b) => a - b); // [1, 2, 3, 4, 5, 6]
✅ 去重逻辑(最容易出错的地方!)
-
外层去重:
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--;- 确保下一次的
left和right指向的是新值。
- 确保下一次的
❗ 注意:必须先
left++和right--,再判断重复,否则会漏掉有效解。
✅ 指针移动逻辑
| 当前三数之和 | 操作 | 原因 |
|---|---|---|
sum === 0 | 记录 + 双移 | 找到解,继续搜索其他组合 |
sum < 0 | left++ | 需要更大的数 |
sum > 0 | right-- | 需要更小的数 |
⏱️ 复杂度分析
-
时间复杂度:
- 排序:O(n log n)
- 外层循环 O(n) × 内层双指针 O(n) → O(n²)
- 总计:O(n²)
-
空间复杂度:
- 仅使用常量额外空间(不计结果数组)→ O(1)
相比暴力 O(n³),这是质的飞跃!
✅ 总结:三数之和的“黄金模板”
- 排序 → 启用双指针 + 便于去重
- 固定一数 → 将问题降维
- 双指针夹逼 → 利用有序性高效搜索
- 双重去重 → 外层跳起点,内层跳重复值
这套思路不仅适用于三数之和,还可扩展至 四数之和(4Sum) 、最接近的三数之和 等变种问题。
🧪 小练习
尝试修改此代码,解决以下问题:
- 找出所有三元组,使其和等于目标值
target - 返回三元组的个数(而非具体组合)
- 允许重复三元组(仅用于理解去重的重要性)
掌握三数之和,就掌握了排序 + 双指针 + 去重这一经典算法范式。它不仅是面试高频题,更是提升代码严谨性与效率思维的绝佳训练场。