算法题 双指针
: 三数之和

解题思路 : 排序 + 双指针
要解决这道题,我们可以采用排序 + 双指针的方法:该问题要求找出所有和为 0 且不重复的三元组。核心思路是将“三数之和”转化为固定一个值之后的“两数之和”,利用排序和双指针来高效解决,同时处理重复元素以满足“不重复”的要求。
1. 排序:先对数组进行升序排序。排序后,方便我们利用双指针缩小范围,也便于处理重复元素(相同的元素会相邻)。
2. 固定第一个数 a :遍历数组,固定第一个数 nums[i] (记为 a )。为了避免无意义的计算,当 a > 0 时可以直接 break(因为数组已升序,后面的数都更大,三数之和不可能为 0 );同时 a 要满足 a <= 0 (否则三数之和必然大于 0 )。
3. 双指针找另外两个数:在固定 a 之后,剩下的两个数需要满足 b + c = -a 。我们在 a 后面的区间 [i+1, n-1] 中,用左指针 left (初始为 i+1 )和右指针 right (初始为 n-1 )来寻找这两个数:
- 若 nums[left] + nums[right] > -a ,说明和太大,将 right 左移(减小和);
- 若 nums[left] + nums[right] < -a ,说明和太小,将 left 右移(增大和);
- 若 nums[left] + nums[right] == -a ,则找到一个有效三元组 [a, nums[left], nums[right]] 。
4. 去重处理:
- 找到一个三元组后, left 和 right 要跳过重复的元素(否则会得到重复的三元组);
- 固定 a 的指针 i ,在遍历完一次后也要跳过重复的元素。
解题代码:
class Solution {
public:vector<vector<int>> threeSum(vector<int>& nums) {vector<vector<int>> ret; // 存储最终结果的二维数组sort(nums.begin(), nums.end()); // 步骤1:对数组排序int n = nums.size();for(int i = 0; i < n; ) { // 步骤2:固定第一个数a(nums[i])if(nums[i] > 0) break; // a>0时,后续三数之和必大于0,直接退出循环int left = i + 1; // 左指针,从i的下一个位置开始int right = n - 1; // 右指针,从数组末尾开始int target = -nums[i]; // 另外两个数的和需要等于 -awhile(left < right) { // 双指针循环,寻找b和cif(nums[left] + nums[right] > target) {right--; // 和太大,右指针左移} else if(nums[left] + nums[right] < target) {left++; // 和太小,左指针右移} else {// 找到有效三元组,存入结果ret.push_back({nums[i], nums[left], nums[right]});left++;right--;// 跳过left的重复元素while(left < right && nums[left] == nums[left - 1]) left++;// 跳过right的重复元素while(left < right && nums[right] == nums[right + 1]) right--;}}i++;// 跳过i的重复元素while(i < n && nums[i] == nums[i - 1]) i++;}return ret;}
};
- 排序操作: sort(nums.begin(), nums.end()) 对数组升序排序,为后续双指针和去重奠定基础。
- 遍历固定 a : for 循环中 i 是固定 a 的指针,循环内先判断 nums[i] > 0 直接 break,避免无效计算。
- 双指针逻辑: while(left < right) 循环中,通过调整 left 和 right 的位置来寻找和为 target (即 -a )的两个数。
- 去重细节:
- 找到三元组后, left++ 和 right-- 后,通过 while 循环跳过与上一个元素相同的 left 和 right , while 循环中为了防止越界必须先判断 left<right ;
- i 遍历后,通过 while 循环跳过与上一个元素相同的 i ,避免重复的 a 导致重复三元组。
- 最后 i++ 后可能会存在越界的可能 , 所以在 while 循环中需要先判断 i<n
注意事项:
1. 去重细节
- 固定数 a 的去重:当遍历到 nums[i] 时,如果它和前一个元素 nums[i-1] 相同,需要跳过,否则会生成重复的三元组。例如数组 [0,0,0,0] ,若不跳过重复的 i ,会多次处理相同的 a ,导致结果中出现多个 [0,0,0] 。
- 双指针的去重:找到一组满足条件的 (left, right) 后, left 需要跳过后续所有相同的元素, right 也需要跳过前面所有相同的元素。比如数组 [-2,0,0,2,2] ,当找到 -2,0,2 后, left 要从第二个 0 跳到 2 , right 要从第二个 2 跳到第一个 2 的前一个位置,避免重复计算 [-2,0,2] 。
2. 边界条件
- a > 0 的提前终止:因为数组已经排序,当 nums[i] > 0 时,后面的数都大于等于 nums[i] ,三数之和必然大于 0 ,可以直接终止循环,减少无效计算。
- 双指针的范围控制:要保证 left < right ,否则会出现指针越界或重复计算的情况。
3. 数组长度的限制
- 题目中说明 3 <= nums.length <= 3000 ,需要确保代码在处理这个长度范围内的数组时不会出现性能问题,而 排序 + 双指针 的方法时间复杂度为O(n^2),在这个数据规模下是可行的。
4. 结果的存储
- 需要用一个二维数组来存储所有符合条件的三元组,每次找到符合条件的三元组时,要将其正确地添加到结果数组中,注意元素的顺序(按照 a, b, c 的顺序,即 nums[i], nums[left], nums[right] )。
这种方法的时间复杂度为 O(n^2)(排序为 O(n\log n),外层遍历 O(n),内层双指针 O(n),整体由 O(n^2) 主导),空间复杂度为 O(\log n)(主要为排序的空间开销),能够高效解决该问题
