优选算法 力扣 18. 四数之和 双指针算法的进化 优化时间复杂度 C++ 题解 每日一题
文章目录
- 一、题目描述
- 二、为什么这道题值得你花几分钟看懂?
- 三、题目拆解:提取其中的关键点
- 四、明确思路:从暴力到优化
- 五、算法实现:固定双元素 + 双指针
- 六、C++代码实现:一步步拆解
- 代码拆解
- 时间复杂度
- 空间复杂度
- 七、实现过程中的坑点总结
- 八、举一反三
- 九、下题预告
一、题目描述
题目链接:四数之和
题目描述:
示例 1:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
示例 2:
输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]
提示:
1 <= nums.length <= 200
10^9 <= nums[i] <= 10^9
10^9 <= target <= 10^9
二、为什么这道题值得你花几分钟看懂?
四数之和作为 LeetCode 第 18 题,是多指针算法的进阶经典题,几乎是大厂算法面试的“常驻嘉宾”。它在三数之和的基础上增加了一层复杂度,能帮你彻底掌握“固定元素 + 双指针”的核心套路,而这种思路是解决 N 数之和、组合求和等一系列问题的关键。
从能力提升来看,这道题能让你深刻理解:
- 如何在多层循环中高效去重,避免重复解(这是很多人卡壳的核心难点);
- 如何通过问题转化降低复杂度(将四数之和转化为两数之和);
- 如何处理数值溢出等边界问题(int 范围下的大数计算陷阱)。
学会这道题,你能举一反三解决:
- 三数之和(LeetCode 15)
- 四数相加 II(LeetCode 454)
- 组合总和(LeetCode 39)等类似问题
生活中也有很多场景能用到这种思路,比如电商平台的“满减组合推荐”(从商品列表中找出多个商品总价满足目标优惠)、财务系统的“多笔交易总和核对”等,核心都是“在一堆数字中找到满足目标和的组合”。
三、题目拆解:提取其中的关键点
结合代码框架和题目要求,核心要素如下:
- 输入是一个整数数组
nums
和目标值target
,数组长度在 1~200 之间,数值范围达 ±10^9(需注意溢出风险)。 - 需返回所有不重复的四元组
[a,b,c,d]
,满足四个数之和等于target
,且索引互不相同。 - 四元组的“不重复”指元素值一一对应相同(如
[2,2,2,2]
即使索引不同也只算一个)。
关键点提炼:
- 多层循环:通过两层循环固定前两个数,再用双指针找后两个数。
- 去重逻辑:对固定的两个数和双指针找到的数分别去重,避免重复四元组。
- 溢出处理:计算目标和时使用
long long
防止整数溢出。 - 边界控制:确保指针索引合法(不越界、不重复)。
四、明确思路:从暴力到优化
1. 暴力解法的困境
最直观的思路是四层循环遍历所有可能的四元组,检查和是否等于 target
,但时间复杂度为 O(n⁴),在 n=200 时会达到 200⁴=1.6×10⁹ 次操作,显然超时。
2. 优化核心:固定 + 双指针
借鉴三数之和的思路,通过固定两个元素,将问题转化为两数之和,把时间复杂度降至 O(n³):
- 先对数组排序(为去重和双指针移动铺路)。
- 外层循环固定第一个元素
nums[i]
,内层循环固定第二个元素nums[j]
(j > i
)。 - 剩余两个元素用双指针在
[j+1, n-1]
范围内寻找,目标和为target - nums[i] - nums[j]
。
3. 去重的必要性
排序后数组中可能有重复元素,若不处理会导致重复四元组。例如 nums = [2,2,2,2,2]
,固定不同位置的 2
会生成相同的四元组,因此需要在固定元素和移动指针时跳过重复值。
4. 溢出防护
由于数值范围达 ±10⁹,四数之和可能超过 int 类型的最大值(2¹⁴⁷⁴⁸³⁶⁴⁷),因此计算中间目标和时需用 long long
类型。
五、算法实现:固定双元素 + 双指针
核心逻辑:
- 排序数组,为去重和双指针移动提供基础。
- 两层循环固定前两个元素
nums[i]
和nums[j]
(i < j
)。 - 计算目标和
aim = target - nums[i] - nums[j]
,用双指针left
(j+1
)和right
(n-1
)寻找和为aim
的两个数。 - 找到符合条件的四元组后,移动指针并去重;若未找到,根据当前和与
aim
的大小关系移动指针。 - 对固定元素
i
和j
进行去重,避免重复计算。
六、C++代码实现:一步步拆解
完整代码(附详细注释):
#include <vector>
#include <algorithm>
using namespace std;class Solution {
public:vector<vector<int>> fourSum(vector<int>& nums, int target) {vector<vector<int>> ret; // 存储结果// 1.排序:为去重和双指针移动做准备sort(nums.begin(), nums.end());int n = nums.size(); // 数组长度// 2.外层循环固定第一个元素a(nums[i])for (int i = 0; i < n;) {// 3.内层循环固定第二个元素b(nums[j]),j > ifor (int j = i + 1; j < n;) {// 4.双指针寻找c和d,范围是[j+1, n-1]int left = j + 1, right = n - 1;// 计算目标和:用long long避免溢出long long aim = (long long)target - nums[i] - nums[j];// 双指针循环while (left < right) {int sum = nums[left] + nums[right];if (sum < aim) {// 和偏小,左指针右移增大总和left++;} else if (sum > aim) {// 和偏大,右指针左移减小总和right--;} else {// 找到符合条件的四元组,加入结果集ret.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--;}}}// 去重三:跳过第二个固定元素j的重复值j++;while (j < n && nums[j] == nums[j - 1]) {j++;}}// 去重四:跳过第一个固定元素i的重复值i++;while (i < n && nums[i] == nums[i - 1]) {i++;}}return ret;}
};
代码拆解
1. 排序预处理
sort(nums.begin(), nums.end());
作用:
- 使重复元素相邻,便于后续去重;
- 保证双指针移动时能根据和的大小调整方向(左指针右移增大和,右指针左移减小和)。
2. 固定元素 i
和 j
的循环
for (int i = 0; i < n;) { // 固定第一个元素for (int j = i + 1; j < n;) { // 固定第二个元素,j > i确保索引不同// ... 双指针逻辑 ...}
}
核心设计:
i
和j
的循环条件不带自增(i++
和j++
放在循环体内),为去重留空间;j = i + 1
确保j
在i
右侧,避免索引重复。
3. 双指针逻辑
int left = j + 1, right = n - 1; // 左指针在j右侧,右指针在末尾
long long aim = (long long)target - nums[i] - nums[j]; // 目标和while (left < right) { // 左指针 < 右指针才有效int sum = nums[left] + nums[right];if (sum < aim) left++; // 和太小,左指针右移else if (sum > aim) right--; // 和太大,右指针左移else { // 找到符合条件的组合ret.push_back({nums[i], nums[j], nums[left++], nums[right--]});// ... 去重逻辑 ...}
}
关键细节:
aim
用long long
计算,防止target - nums[i] - nums[j]
溢出(例如当target=1e9
且nums[i]
和nums[j]
均为1e9
时,结果为负数,用 int 会溢出);- 找到四元组后,
left++
和right--
同时移动,继续寻找下一组可能的组合。
4. 四层去重逻辑
// 1. 左指针去重:跳过与前一个元素相同的值
while (left < right && nums[left] == nums[left - 1]) left++;// 2. 右指针去重:跳过与后一个元素相同的值
while (left < right && nums[right] == nums[right + 1]) right--;// 3. 第二个固定元素j去重:跳过与前一个j相同的值
j++;
while (j < n && nums[j] == nums[j - 1]) j++;// 4. 第一个固定元素i去重:跳过与前一个i相同的值
i++;
while (i < n && nums[i] == nums[i - 1]) i++;
去重原理:
- 排序后重复元素相邻,因此只需比较当前元素与前一个元素(
i
、j
、left
)或后一个元素(right
); - 去重操作放在指针移动后,确保先处理当前元素,再跳过重复值。
示例运行过程(以示例 1 为例):
输入:nums = [1,0,-1,0,-2,2]
,target = 0
排序后:[-2, -1, 0, 0, 1, 2]
-
i=0
(nums[i]=-2
),j=1
(nums[j]=-1
):aim = 0 - (-2) - (-1) = 3
- 双指针
left=2
(0),right=5
(2),sum=0+2=2 < 3
→left++
left=3
(0),right=5
(2),sum=0+2=2 < 3
→left++
left=4
(1),right=5
(2),sum=1+2=3 == aim
→ 记录[-2,-1,1,2]
- 去重后
left=5
,right=4
,双指针循环结束。
-
后续
i
和j
继续循环,依次找到[-2,0,0,2]
和[-1,0,0,1]
,最终返回这三个四元组。
时间复杂度
操作类型 | 时间复杂度 | 说明 |
---|---|---|
排序预处理 | O(n log n) | 快速排序的时间复杂度 |
外层两层循环 | O(n²) | 固定两个元素的循环 |
内层双指针循环 | O(n) | 每个内层循环中双指针遍历 |
总计 | O(n³) | 由 O(n² × n) 主导 |
空间复杂度
- O(log n) 至 O(n):取决于排序算法的空间复杂度(快速排序为 O(log n),归并排序为 O(n)),结果存储不计入额外空间。
七、实现过程中的坑点总结
-
整数溢出问题
- 错误写法:
int aim = target - nums[i] - nums[j];
- 问题:当
target
和nums[i]
、nums[j]
取值较大时,计算结果可能超出 int 范围(如1e9 - (-1e9) - (-1e9) = 3e9 > 2e9
)。 - 解决:用
long long
存储中间结果:long long aim = (long long)target - nums[i] - nums[j];
- 错误写法:
-
去重时机错误
- 错误写法:在固定元素前先去重(如
i
初始化为 1,跳过nums[i] == nums[i-1]
)。 - 问题:会漏掉第一个元素的处理(如
i=0
时的元素)。 - 解决:先处理当前元素,再在循环体内去重(
i++
后检查nums[i] == nums[i-1]
)。
- 错误写法:在固定元素前先去重(如
-
指针越界
- 错误写法:去重时未判断
left < right
或j < n
。 - 问题:可能导致指针超出数组范围(如
left
超过right
后仍访问nums[left]
)。 - 解决:去重逻辑需带边界条件(如
while (left < right && ...)
)。
- 错误写法:去重时未判断
-
索引重复
- 错误写法:
j
的初始值为i
而非i+1
。 - 问题:导致
i
和j
索引相同,不符合“四元组索引互不相同”的要求。 - 解决:确保
j = i + 1
,left = j + 1
,保证四个索引严格递增。
- 错误写法:
八、举一反三
掌握四数之和的思路后,可轻松解决以下问题:
- 三数之和(LeetCode 15):固定一个元素,用双指针找另外两个,目标和为 0。
- 五数之和:固定三个元素,转化为两数之和,时间复杂度 O(n⁴)。
- 组合总和 II(LeetCode 40):类似多数之和,需去重且元素只能使用一次。
核心规律:k 数之和问题可通过固定 k-2 个元素,转化为两数之和,时间复杂度为 O(n^(k-1)),且去重逻辑需针对每个固定元素和双指针分别处理。
九、下题预告
明天将讲解 LeetCode 209. 长度最小的子数组,这道题是滑动窗口算法的经典入门题,核心是如何用双指针在 O(n) 时间内找到和大于等于目标值的最短子数组。它和今天的四数之和同为双指针技巧,但思路截然不同(一个是固定元素找组合,一个是动态调整窗口范围),敬请期待!
如果觉得这篇解析有帮助,不妨:
🌟 点个赞,让更多人看到这份清晰思路
⭐ 收个藏,下次复习时能快速找回灵感
👀 加个关注,明天的滑动窗口解析更精彩~
这是封面原图: