一、题目描述
题目链接:三数之和
题目描述:
示例 1:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
示例 2:
输入:nums = [0,1,1]
输出:[]
示例 3:
输入:nums = [0,0,0]
输出:[]
提示:
0 <= nums.length <= 3000
-10^5 <= nums[i] <= 10^5
二、解题思路
三数之和问题可以基于两数之和的双指针思路进行扩展,核心是固定一个元素,将问题转化为两数之和。具体步骤如下:
- 排序预处理:对数组进行排序,为后续去重和双指针移动提供基础。
- 固定第三个元素:从数组末尾开始,依次固定一个元素
nums[i]
,则问题转化为在[0, i-1]
范围内寻找两个元素,使得它们的和为-nums[i]
。 - 双指针搜索:在
[0, i-1]
范围内使用左右双指针(left
起始于 0,right
起始于i-1
),根据三数之和与 0 的大小关系移动指针:- 若和小于 0:左指针右移(增大总和)
- 若和大于 0:右指针左移(减小总和)
- 若和等于 0:记录三元组,并移动指针跳过重复元素
- 去重优化:在固定元素和移动双指针时,通过跳过重复值避免生成重复三元组。
三、代码实现
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 排序,为去重和双指针做准备
int size = nums.size();
vector<vector<int>> ret; // 存储结果
int i = size - 1; // 从末尾开始固定第三个元素
while (i >= 0) {
int left = 0, right = i - 1; // 双指针范围:[0, i-1]
while (left < right) {
int sum = nums[left] + nums[right] + nums[i];
if (sum < 0) {
left++; // 总和偏小,左指针右移增大总和
} else if (sum > 0) {
right--; // 总和偏大,右指针左移减小总和
} else {
// 找到符合条件的三元组
ret.push_back({nums[left], nums[right], nums[i]});
left++;
right--;
// 跳过左指针重复元素
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
// 跳过右指针重复元素
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
}
}
// 固定元素去重:跳过与当前i相同的元素
i--;
while (i >= 0 && nums[i + 1] == nums[i]) {
i--;
}
}
return ret;
}
};
四、越界问题深度分析
在三数之和的双指针实现中,越界风险主要集中在指针移动和数组访问的边界处理上,以下是关键分析:
1. 固定元素 i
的越界防护
- 初始值:
i
从size-1
开始(数组最后一个元素),确保nums[i]
访问有效。 - 循环条件:
while (i >= 0)
确保i
不会小于 0,避免访问nums[-1]
等无效索引。 - 去重逻辑:
while (i >= 0 && nums[i + 1] == nums[i])
中,i >= 0
是前置条件,防止i-1
时i+1
越界(例如i=0
时i+1=1
有效,但需确保i
不小于 0 才能进入循环)。
2. 双指针 left
和 right
的越界防护
- 初始范围:
left
从 0 开始,right
从i-1
开始,由于i >= 0
,right
初始值最大为size-2
(当i=size-1
时),确保nums[right]
有效。 - 循环条件:
while (left < right)
保证left
始终在right
左侧,避免两者交叉后访问无效索引(例如left = right + 1
时的越界)。 - 去重逻辑:
while (left < right && nums[left] == nums[left - 1])
中,left < right
确保left
不会超过右边界,left - 1 >= 0
(因left
初始为 0,且只有left++
后才可能进入该循环,此时left >= 1
)。while (left < right && nums[right] == nums[right + 1])
中,right + 1 <= i-1 + 1 = i
(因right
初始为i-1
),而i <= size-1
,故right + 1 <= size-1
,避免访问越界。
3. 总结:为何此代码不会越界?
- 所有数组访问(
nums[left]
、nums[right]
、nums[i]
)的索引均通过循环条件和指针移动逻辑被限制在[0, size-1]
范围内。 - 去重逻辑中的指针移动均有前置条件(如
left < right
、i >= 0
),防止指针超出有效范围。
五、去重逻辑解析
三数之和的核心难点之一是避免重复三元组,代码通过三层去重确保结果唯一:
固定元素去重:
- 当
nums[i]
与nums[i+1]
相同时,跳过当前i
(i--
),避免因固定相同元素导致重复三元组。 - 例如:
nums = [-1, -1, 2]
,若不去重,i=2
(值 2)时会重复记录[-1, -1, 2]
。
- 当
左指针去重:
- 找到有效三元组后,若
nums[left]
与nums[left-1]
相同,left++
跳过,避免重复使用相同元素。 - 例如:
nums = [-2, 0, 0, 2, 2]
,避免因left
指向不同位置的 0 或 2 导致重复。
- 找到有效三元组后,若
右指针去重:
- 类似左指针,若
nums[right]
与nums[right+1]
相同,right--
跳过。
- 类似左指针,若
六、复杂度分析
复杂度类型 | 具体值 | 说明 |
---|---|---|
时间复杂度 | O(n²) | 排序耗时 O(n log n),外层循环 O(n),内层双指针循环 O(n),整体由 O(n²) 主导 |
空间复杂度 | O(log n) 或 O(n) | 取决于排序算法的空间复杂度(如快速排序为 O(log n),归并排序为 O(n)),结果存储不计入额外空间 |
七、举一反三
三数之和的“固定一个元素 + 双指针”思路可推广到:
- N数之和问题(如明天的四数之和):固定 N-2 个元素,转化为两数之和。
- 带约束的组合问题:如寻找和为目标值的 k 个元素,且元素不重复。
下一题预告:LeetCode 18. 四数之和!这道题在三数之和基础上增加了一个固定元素,需要处理更多层的去重逻辑和边界条件,同时需注意目标值不是固定的 0 而是输入的 target,进一步考验对多指针和去重的掌握。
✨ 如果你觉得这篇解析有帮助,不妨:
🌟 点个赞,让更多人看到这份实用思路
⭐ 收个藏,下次遇到类似问题能快速复习
👀 加个关注,明天的三数之和解析更精彩
这是封面原图嘿嘿