一、环境说明
- 本文是 LeetCode 15题 : 三数之和,使用c语言实现。
- 快排+对撞指针。
- 测试环境:Visual Studio 2019。
二、代码展示
C
//排序,双指针向中间对撞
int cmp(const void *a,const void *b){
return *(int*)a > *(int*)b;//升序
}
int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) {
qsort(nums, numsSize, sizeof(int), cmp);//快排
int** ans = (int**)malloc(sizeof(int*) * numsSize * (numsSize-1)/2);//Cn-2种组合
*returnSize = 0;//返回值大小从0开始
*returnColumnSizes = (int*)malloc(sizeof(int) * numsSize * (numsSize-1)/2);//三元组最大数量
for (int i = 0; i < numsSize; ++i) {
if (nums[i] > 0) {//剪枝
break;
}
if (i > 0 && nums[i] == nums[i - 1]) {//当前数和上一个数重复,结果重复。
continue;
}
int left = i + 1, right = numsSize - 1;//左指针是第二个数,从第一个数的右一位出发向右; 右指针是第三个数,从右边界向左。
while (left < right) {//左右不能颠倒
int sum = nums[left] + nums[right] + nums[i];
if (sum == 0) {//保存一个答案
ans[*returnSize] = (int*)malloc(sizeof(int) * 3);
(*returnColumnSizes)[*returnSize] = 3;
ans[*returnSize][0] = nums[i];
ans[*returnSize][1] = nums[left];
ans[*returnSize][2] = nums[right];
(*returnSize)++;
while ((left < right) && (nums[left] == nums[left+1])){//下一个数和当前重复,结果重复。
left++;//最后一个重复的数
}
left++;//第一个不重复的数
while ((left < right) && (nums[right] == nums[right-1])){//是上面过程的反向。
right--;
}
right--;
}
else if (sum > 0) {//大了
right--;
}
else {//小了
left++;
}
}
}
return ans;
}
C++
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(),nums.end());
int n = nums.size();
vector<vector<int>> ans;
for(int i = 0;i<n-2;i++) {
if(nums[i]>0) break;//剪枝
if(i&&nums[i]==nums[i-1]) continue;
for(int l = i+1,r = n-1;l<r;){
if(nums[l]==nums[l-1]&&l>i+1) {
l++;
continue;
}
if(nums[i]+nums[l]+nums[r]> 0 )r--;
else if(nums[i]+nums[l]+nums[r]<0) l++;
else {
ans.push_back({nums[i],nums[l],nums[r]});
l++,r--;
}
}
}
return ans;
}
};
完整代码(ACM模式) VSCode & C++ & Win11
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
int n = nums.size();
vector<vector<int>> ans;
for(int i = 0; i < n - 2; i ++) {
if(nums[i] > 0) break;//剪枝
if(i && nums[i] == nums[i - 1]) continue;
for(int l = i + 1,r = n - 1; l < r;) {
if(nums[l] == nums[l - 1] && l > i + 1) {
l ++;
continue;
}
if (nums[i] + nums[l] + nums[r] > 0 ) r --;
else if (nums[i] + nums[l] + nums[r] < 0) l++;
else {
ans.push_back({nums[i], nums[l], nums[r]});
l ++, r --;
}
}
}
return ans;
}
};
int main() {
Solution S;
vector<int> nums; // 测试数据
int temp; // 每次读入一个数
while (scanf("%d", &temp) != EOF) { // 依次读入数据,以读入ctrl + z结束(仅windows + vscode环境,其他环境可能要读入两个ctrl + z)。
nums.push_back(temp); // 如果对输入数据的规范有疑惑,请抄原题的输入样例,如:-1 0 1 2 -1 -4 Ctrl+Z。欢迎评论区提问。
}
vector<vector<int>> ans = S.threeSum(nums);
for (int i = 0; i < ans.size(); i ++) {
printf("第%d个结果: {%d, %d, %d}\n", i + 1, ans[i][0], ans[i][1], ans[i][2]);
}
return 0;
}
三、思路分析
- 如题,无序数组,找三数之和等于 0 0 0。容易想到三重循环,依次确定三个数,得到答案,时间复杂度 O ( n 3 ) O(n^3) O(n3)。考虑问题规模3000,需要 27 × 1 0 9 27\times10^9 27×109次执行,TLE。要降低时间复杂度。
- 转换思路,我们让这个数组有序,是不是就可以用双指针来解题了呢?答案是肯定的。
3.1 双指针思路:
- 快排 n u m s nums nums数组,一次遍历 n u m s nums nums,当前遍历的位置是 i i i。
- i i i表示第一个数, i i i也是最外层遍历的位置;
- l e f t left left表示第二个数, l e f t left left从 i + 1 i+1 i+1开始往右;
- r i g h t right right表示第三个数, r i g h t right right从 n u m s S i z e − 1 numsSize - 1 numsSize−1开始往左。
3.2思路解释:
- 三数之和 s u m = n u m s [ i ] + n u m s [ l e f t ] + n u m s [ r i g h t ] sum=nums[i]+nums[left]+nums[right] sum=nums[i]+nums[left]+nums[right]。
- 对于内层 l e f t left left和 r i g h t right right的双指针循环, n u m s [ i ] nums[i] nums[i]是固定的。
- 如果 s u m > 0 sum>0 sum>0,为了让 s u m sum sum趋于0, r i g h t right right左移一位;
- 如果 s u m < 0 sum<0 sum<0,为了让 s u m sum sum趋于0, l e f t left left右移一位。
- 为了避免重复的三元组,还有额外的操作,见代码和注释。
四、致语
- 理解思路很重要!
- 博主欢迎读者在评论区留言,作为日更博主,看到就会回复的。
五、AC
六、复杂度分析
- 时间复杂度: O ( n 2 ) O(n^2) O(n2) , n u m s nums nums数组的大小= n n n。快排时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)。一次遍历 n u m s nums nums的时间复杂度是 O ( n ) O(n) O(n),使用双指针 l e f t left left和 r i g h t right right对撞的时间复杂度是 O ( n ) O(n) O(n),二者嵌套的时间复杂度是 O ( n 2 ) O(n^2) O(n2)。实际时间复杂度是 O ( n 2 + n l o g n ) O(n^2+nlogn) O(n2+nlogn)。忽略常数(较低数量级),时间复杂度 O ( n 2 ) O(n^2) O(n2)
- 空间复杂度: O ( l o g n ) O(logn) O(logn),快排的递归调用压栈 O ( l o g n ) O(logn) O(logn)次,空间复杂度是 O ( l o g n ) O(logn) O(logn)。