📌 题目链接:leetcode.cn/problems/3s…
🔍 难度:中等 | 🏷️ 标签:数组、双指针、排序、去重
⏱️ 目标时间复杂度:O(n²)
💾 空间复杂度:O(log n)(忽略结果存储空间,仅考虑排序所需栈空间)
💡 题目分析
给定一个整数数组 nums,需找出所有 和为 0 且不重复 的三元组 [nums[i], nums[j], nums[k]]。要求:
- 三元组中元素下标互不相同;
- 答案不能包含重复的三元组(例如
[-1,0,1]和[1,0,-1]被视为重复)。
📌 关键难点:如何高效地避免重复解?暴力枚举 O(n³) 不仅超时,还难以处理去重逻辑。
🧠 核心算法及代码讲解
我们采用 排序 + 双指针 的经典策略,将问题从三维降维到二维,同时天然支持去重。
✅ 步骤详解
1. 排序预处理
sort(nums.begin(), nums.end());
- 将数组升序排序,使相同元素相邻,便于后续去重;
- 同时赋予双指针移动方向性:左指针右移 → 值增大;右指针左移 → 值减小。
2. 外层循环固定第一个数 nums[i]
for (int i = 0; i < n; i++) {
if (nums[i] > 0) return ret;
if (i > 0 && nums[i] == nums[i - 1]) continue;
- 剪枝优化:若
nums[i] > 0,由于数组已排序,后续所有数 ≥nums[i] > 0,三数之和不可能为 0,直接终止。 - 去重第一个数:若当前
nums[i]与前一个相同,跳过,防止生成重复三元组。
3. 双指针查找剩余两数
设 left = i + 1,right = n - 1,计算:
int sum = nums[i] + nums[left] + nums[right];
- sum == 0:找到有效解,加入结果,并同时移动左右指针跳过重复值;
- sum < 0:和太小,左指针右移以增大;
- sum > 0:和太大,右指针左移以减小。
4. 关键去重逻辑(面试高频考点!)
⚠️ 很多人只在外层去重,但内层去重同样重要!
// 找到解后,跳过重复的 left 和 right
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++;
right--;
- 这两行确保:即使有多个相同值,也只取一组;
- 必须先跳过重复,再移动指针,否则可能漏掉边界情况(如
[0,0,0,0])。
🧪 解题思路总结
- 排序 → 为双指针和去重奠定基础;
- 固定第一个数 → 外层遍历,剪枝 + 去重;
- 双指针夹逼 → 在剩余区间寻找满足条件的两个数;
- 双重去重 → 外层防首元素重复,内层防第二、第三元素重复;
- 提前终止 → 第一个数 > 0 时直接返回,极大提升效率。
💬 面试提示:当被问“为什么排序后不会漏解?”
回答:因为三元组是无序的,排序只是改变了枚举顺序,不影响组合的存在性。而通过固定第一个数 + 双指针,我们系统地覆盖了所有可能的组合,且通过去重保证唯一性。
📊 算法复杂度分析
| 项目 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n²) | 排序 O(n log n),外层循环 O(n),内层双指针 O(n),主导项为 O(n²) |
| 空间复杂度 | O(log n) | 忽略结果存储,仅考虑快排递归栈空间(C++ std::sort 通常为 introsort) |
✅ 满足 LeetCode 对时间和空间的要求,是最优解法之一。
💻 完整可运行代码(含测试用例)
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ret;
if (n < 3) return ret; // 特判:元素不足三个直接返回
sort(nums.begin(), nums.end()); // 排序预处理
for (int i = 0; i < n; i++) {
if (nums[i] > 0) return ret; // 剪枝:第一个数大于0,无解
if (i > 0 && nums[i] == nums[i - 1]) continue; // 去重:跳过重复的第一个数
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
ret.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--;
} else if (sum < 0) {
left++; // 和太小,左指针右移
} else {
right--; // 和太大,右指针左移
}
}
}
return ret;
}
};
int main() {
Solution solution;
vector<int> nums1 = {-1, 0, 1, 2, -1, -4};
vector<vector<int>> result1 = solution.threeSum(nums1);
cout << "测试用例1: [-1,0,1,2,-1,-4]" << endl;
for (auto& triplet : result1) {
cout << "[" << triplet[0] << "," << triplet[1] << "," << triplet[2] << "]" << endl;
}
vector<int> nums2 = {0, 1, 1};
vector<vector<int>> result2 = solution.threeSum(nums2);
cout << "测试用例2: [0,1,1]" << endl;
for (auto& triplet : result2) {
cout << "[" << triplet[0] << "," << triplet[1] << "," << triplet[2] << "]" << endl;
}
vector<int> nums3 = {0, 0, 0};
vector<vector<int>> result3 = solution.threeSum(nums3);
cout << "测试用例3: [0,0,0]" << endl;
for (auto& triplet : result3) {
cout << "[" << triplet[0] << "," << triplet[1] << "," << triplet[2] << "]" << endl;
}
return 0;
}
输出示例:
测试用例1: [-1,0,1,2,-1,-4]
[-1,-1,2]
[-1,0,1]
测试用例2: [0,1,1]
测试用例3: [0,0,0]
[0,0,0]
🎯 面试延伸思考
-
如果题目改为“四数之和”?
→ 同理:排序 + 两层外层循环 + 双指针,时间复杂度 O(n³),注意多层去重。 -
能否用哈希表解决?
→ 理论上可以(固定两个数,查第三个),但去重极其困难,且空间开销大,不推荐。 -
为什么不用 set 去重?
→ 虽然可行,但会增加 O(k log k) 的插入开销(k 为结果数量),且违背“原地、高效”的设计哲学。 -
边界测试点有哪些?
- 全零数组
[0,0,0,0] - 无解数组
[1,2,3] - 重复元素多的数组
[-2,-2,-1,-1,0,0,1,1,2,2]
- 全零数组
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📣 下一期预告:LeetCode 热题 100 第26题 —— 删除有序数组中的重复项(简单)
🔹 题目:给你一个升序排列的数组
nums,请你原地删除重复出现的元素,使得每个元素只出现一次,并返回删除后数组的新长度。🔹 核心思路:快慢双指针——快指针遍历,慢指针记录不重复元素的位置。
🔹 考点:双指针、原地修改、边界处理。
🔹 难度:简单,但却是“数组去重”类问题的基石,高频面试题!
💡 提示:不要使用额外数组空间,必须在 O(1) 额外空间下完成!
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!