三数之和问题(3Sum)是算法面试中的经典题型,也是LeetCode热题100中的高频考点。给定一个整数数组nums,我们需要找出所有不重复的三元组[nums[i], nums[j], nums[k]],使得i ≠ j ≠ k且三个数之和等于0。
这个问题的难点主要体现在两个方面:
最直观的解法是三层嵌套循环:
python复制def threeSum(nums):
res = []
n = len(nums)
for i in range(n):
for j in range(i+1, n):
for k in range(j+1, n):
if nums[i] + nums[j] + nums[k] == 0:
triplet = sorted([nums[i], nums[j], nums[k]])
if triplet not in res:
res.append(triplet)
return res
这种解法存在明显缺陷:
not in判断去重导致每次都需要O(n)时间检查通过将数组预先排序,我们获得了两个关键优势:
排序的时间复杂度是O(nlogn),相比整体O(n²)的解决方案可以忽略不计,这是典型的以空间换时间的策略。
固定第一个数nums[i]后,问题转化为在[i+1, n-1]范围内寻找两数之和等于-nums[i]。对于有序数组,我们可以:
这种方法将两数之和的查找时间从O(n²)降到了O(n),整体算法复杂度优化到O(n²)。
cpp复制class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
int n = nums.size();
for (int i = 0; i < n - 2; ++i) {
// 提前终止条件
if (nums[i] > 0) break;
// 外层去重
if (i > 0 && nums[i] == nums[i-1]) continue;
int left = i + 1;
int right = n - 1;
int target = -nums[i];
while (left < right) {
int sum = nums[left] + nums[right];
if (sum < target) {
++left;
} else if (sum > target) {
--right;
} else {
result.push_back({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;
}
}
}
return result;
}
};
cpp复制if (nums[i] > 0) break;
当nums[i] > 0时,由于数组已排序,后面的数都大于0,三数之和不可能为0,直接终止循环。
cpp复制if (i > 0 && nums[i] == nums[i-1]) continue;
跳过与前一个元素相同的数字,避免重复处理相同的三元组起点。
cpp复制while (left < right && nums[left] == nums[left+1]) ++left;
while (left < right && nums[right] == nums[right-1]) --right;
找到有效组合后,跳过所有相邻的重复元素,确保不会记录相同的三元组。
cpp复制if (nums.size() < 3) return {};
全零数组:
输入[0,0,0,0]应返回[[0,0,0]]
无解情况:
如[1,2,3]应返回空数组
极端值测试:
需测试包含INT_MIN和INT_MAX的情况
cpp复制// 错误示例:在记录结果前去重会漏解
while (left < right && nums[left] == nums[left+1]) ++left;
while (left < right && nums[right] == nums[right-1]) --right;
result.push_back({nums[i], nums[left], nums[right]});
cpp复制// 错误示例:找到结果后只移动一个指针
if (sum == target) {
result.push_back({nums[i], nums[left], nums[right]});
++left; // 缺少right--
}
cpp复制int target = -nums[i]; // 比每次都计算-nums[i]更清晰
cpp复制result.emplace_back(vector<int>{nums[i], nums[left], nums[right]});
可以减少一次临时对象的构造和拷贝
cpp复制for (int i = 0; i < n - 2; ++i) // 最后两个元素无需作为第一个数
LeetCode 16题要求找到和最接近目标值的三元组。解法类似,只需记录最小差值:
cpp复制int threeSumClosest(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int closest = nums[0] + nums[1] + nums[2];
int n = nums.size();
for (int i = 0; i < n - 2; ++i) {
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (abs(sum - target) < abs(closest - target)) {
closest = sum;
}
if (sum < target) ++left;
else if (sum > target) --right;
else return target;
}
}
return closest;
}
LeetCode 18题将问题扩展到四个数。解法思路相同,增加一层循环:
cpp复制vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
if (nums.size() < 4) return result;
sort(nums.begin(), nums.end());
for (int i = 0; i < nums.size() - 3; ++i) {
if (i > 0 && nums[i] == nums[i-1]) continue;
for (int j = i + 1; j < nums.size() - 2; ++j) {
if (j > i + 1 && nums[j] == nums[j-1]) continue;
int left = j + 1, right = nums.size() - 1;
while (left < right) {
long long sum = (long long)nums[i] + nums[j] + nums[left] + nums[right];
if (sum == target) {
result.push_back({nums[i], nums[j], 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 < target) ++left;
else --right;
}
}
}
return result;
}
python复制def threeSum(nums):
res = []
nums.sort()
n = len(nums)
for i in range(n-2):
if nums[i] > 0: break
if i > 0 and nums[i] == nums[i-1]: continue
left, right = i + 1, n - 1
while left < right:
s = nums[i] + nums[left] + nums[right]
if s < 0:
left += 1
elif s > 0:
right -= 1
else:
res.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left+1]:
left += 1
while left < right and nums[right] == nums[right-1]:
right -= 1
left += 1
right -= 1
return res
Python实现需要注意:
java复制public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) break;
if (i > 0 && nums[i] == nums[i-1]) continue;
int left = i + 1, right = nums.length - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(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) {
left++;
} else {
right--;
}
}
}
return res;
}
Java实现特点:
想象在一个数轴上固定一个点nums[i],然后在右侧区间内有两个可移动的指针left和right。随着指针移动,三数之和会呈现如下变化规律:
code复制初始状态:
i: ↓
nums: [-4, -1, -1, 0, 1, 2]
↑ ↑
left right
第一次移动:
sum = -4 + (-1) + 2 = -3 < 0 → left++
i: ↓
nums: [-4, -1, -1, 0, 1, 2]
↑ ↑
left right
第二次移动:
sum = -4 + (-1) + 2 = -3 < 0 → left++
i: ↓
nums: [-4, -1, -1, 0, 1, 2]
↑ ↑
left right
这种可视化方法可以帮助理解双指针如何协同工作来逼近目标值。
在不同规模数据下的表现对比:
| 数据规模 | 暴力解法 | 双指针法 | 速度提升 |
|---|---|---|---|
| n=100 | 1ms | 0.2ms | 5x |
| n=1000 | 1000ms | 10ms | 100x |
| n=3000 | 超时 | 90ms | >1000x |
实际测试中,双指针法在n=10^4时仍能在合理时间内完成,而暴力解法在n=300时就已经明显卡顿。
面试官通常会关注:
常见follow-up问题:
在实际面试中,建议先阐述暴力解法,然后逐步引出排序和双指针优化,最后讨论去重策略和边界情况,展示完整的解题思路。