文章目录
前言
一、一数之和
返回指定元素在数组中的位置,如果不存在则返回 -1 。
1、线性枚举
1)算法描述
直接遍历整个数组,如果找到题目中指定的元素,直接返回对应的下标。
2)时间复杂度
由于需要一次遍历,符合题目要求的整数可能出现在数组最后,最坏时间复杂度为 O ( n ) O(n) O(n)。
3) 算法
class Solution{
public:
vector<int> oneSum(vector<int>& nums, int target){
for(int i = 0; i < nums.size(); ++i){
if(nums[i] == target){
return i;
}
}
return -1;
}
};
二、二数之和
输入一个整数数组和一个目标值 t a r g e t target target ,在数组中查找两个数,使得它们的和正好是给定的目标值 t a r g e t target target。如果有多对数字的和等于 t a r g e t target target ,则输出任意一对即可。
1、暴力枚举
1)算法描述
暴力枚举第一个数,再枚举第二个数,如果两数之和等于 t a r g e t target target,直接返回。
2)时间复杂度
因为要枚举两个数,所以相当于从 n n n个数里面取 2 2 2个数,也就是 C n 2 C_n^2 Cn2,最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)。
3) 算法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int i, j;
int n = nums.size();
for(i = 0; i < n; ++i){
for(j = i + 1; j < n; ++j){
if(nums[i] + nums[j] == target){
return {nums[i], nums[j]};
}
}
}
return {};
}
};
增加一些剪枝操作进行优化
(1)最大剪枝:如果枚举的第一个数加上数组中的最大数都小于
t
a
r
g
e
t
target
target ,说明第二个满足条件的数不可能存在了,跳过本次循环。
(2)最小剪枝:如果当前枚举的最小的两个数的和都大于
t
a
r
g
e
t
target
target ,说明后面所有的数的和都不可能出现两两加和等于
t
a
r
g
e
t
target
target 的情况,直接跳出循环。
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int i, j;
int n = nums.size();
sort(nums.begin(), nums.end());
for(i = 0; i < n; ++i){
// 增加一些剪枝操作
if(nums[i] + nums.back() < target) continue; //(1)
if(nums[i] + nums[i + 1] > target) break; // (2)
for(j = i + 1; j < n; ++j){
if(nums[i] + nums[j] == target){
return {nums[i], nums[j]};
}
}
}
return {};
}
};
2、二分查找
1)算法描述
(1)二分查找适应于有序数组,因此先将整数数组
n
u
m
s
nums
nums进行排序操作;
(2)先遍历第一个数,再在剩下的数里面二分查找第二个数;
2)时间复杂度
时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。
3) 算法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
int i;
int n = nums.size();
int temp = target;
for(i = 0; i < n; ++i){
target = temp - nums[i];
int l = i, r = n - 1;
int mid;
while(l <= r){
mid = (l + r) >> 1;
if(nums[mid] > target) r = mid - 1;
else if(nums[mid] < target) l = mid + 1;
else{
return {nums[i], nums[mid]};
}
}
}
return {};
}
};
3、哈希表
1)算法描述
(1)利用一个哈希表作为辅助;
(2)枚举一遍数组;
(3)每次查找target - nums[i]
在不在哈希表中。如果在,则直接返回{nums[i], target - nums[i]}
;否则,将nums[i]
插入哈希表中;
(4)哈希表的话,可以自己实现冲突的解决,或者直接用 c++ 中的unordered_map
;
2)时间复杂度
第一层枚举的复杂度是 O ( n ) O(n) O(n),哈希表查找的时间复杂度是 O ( 1 ) O(1) O(1),因此总的时间复杂度是 O ( n ) O(n) O(n)。
3) 算法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); ++i) {
if(hash.find(target - nums[i]) != hash.end()) {
return {nums[i], target - nums[i]};
}else {
hash[nums[i]] = 1;
}
}
return {};
}
};
4、双指针
1)算法描述
(1)首先将数组排序,对于排序好的数组进行双指针操作;
(2)定义两个指针
l
l
l 和
r
r
r 分别指向数组首和尾,记录`temp =num[l]+nums[r],当
l
<
r
l<r
l<r 时循环,当
t
e
m
p
>
t
a
r
g
e
t
temp > target
temp>target时说明右边的游标要减小即向左移动一个位置,
t
e
m
p
<
t
a
r
g
e
t
temp < target
temp<target 时,增大左边的游标。
2)时间复杂度
由于两个指针会往固定的方向移动,所以数组每个元素最多会被遍历一次,时间复杂度 O ( n ) O(n) O(n)。
3) 算法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
sort(nums.begin(), nums.end());
int n = nums.size();
int l = 0, r = n - 1;
while(l < r){
int temp = nums[l] + nums[r];
if(temp > target) --r;
else if(temp < target) ++ l;
else{
return {nums[l], nums[r]};
}
}
return {};
}
};
三、三数之和
给你一个包含 n 个整数的数组
nums
,判断nums
中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
经过以上 一数之和 和 二数之和 的分析,我们知道 三数之和 和接下来的 四数之和 乃至 N数之和 都可以通过线性枚举的方法来实现。考虑到时间复杂度,线性枚举的方法不是最优的,对于长数组的题解单纯的线性枚举一定会超时,所以接下来的几个题目不再介绍线性枚举的方法,只介绍相对较优的解法,以供大家参考。
1、线性枚举+双指针
1)算法分析与描述
对于从数组中取出三个数,使其满足a+b+c=0
,可以先枚举出数组中最小的数a
,再在后面的数组中取出满足
t
a
r
g
e
t
=
b
+
c
=
−
a
target = b+c=-a
target=b+c=−a的两个数。要求数组无重复,因此需要先对数组进行排序,也方便枚举出当前最小的a
,遇到相同的a
只要枚举一个就行了。
求两数之和自然可以使用双指针。还可以增加一些剪枝操作,减少时间。
2)时间复杂度
对第一个数a
枚举的时间的复杂度是
O
(
n
)
O(n)
O(n),双指针寻找第二、三个数的时间复杂度是
O
(
n
)
O(n)
O(n),因此总的时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)。
3) 算法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ret;
int n = nums.size();
sort(nums.begin(), nums.end());
// 元素数量不足时
if(n < 3 || nums[0] > 0 || nums[n-1] < 0){
return ret;
}
int i, j, k;
for(i = 0; i < n-2; ++i){
if(i && nums[i] == nums[i-1]){
continue;
}
j = i + 1;
k = n - 1;
while(j < k){
int target = nums[i] + nums[j] + nums[k];
if(target > 0){
--k;
}
else if(target < 0){
++j;
}
else{
ret.push_back({nums[i], nums[j], nums[k]});
++j;
--k;
while(j < k && nums[j] == nums[j-1]) ++j;
while(j < k && nums[k] == nums[k + 1]) --k;
}
}
}
return ret;
}
};
四、四数之和
给你一个由 n 个整数组成的数组
nums
,和一个目标值target
。请你找出并返回满足下述全部条件且不重复的四元组[nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
1、两次线性枚举+双指针
1)算法分析与描述
四数之和和三数之和有异曲同工之妙,三数之和是线性枚举第一个数,而四数之和则需要枚举第一、二两个数,剩下的两个数则可以通过双指针的操作,找到
t
a
r
g
e
t
target
target
t
a
r
g
e
t
=
c
+
d
=
−
(
a
+
b
)
target = c + d = - (a + b)
target=c+d=−(a+b).
2)时间复杂度
两层线性枚举的时间复杂度是 O ( n 2 ) O(n^2) O(n2),双指针的时间复杂度是 O ( n ) O(n) O(n),因此总的时间复杂度是 O ( n 3 ) O(n^3) O(n3)。
3) 算法实现
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
int n = nums.size();
// 简单的剪枝操作
if(n < 4){
return ret;
}
int i, j, l, r;
sort(nums.begin(), nums.end());
for(i = 0; i < n-3; ++i){
if(i > 0 && nums[i] == nums[i-1]) continue;
for(j = i + 1; j < n-2; ++j){
if(j > i+1 && nums[j] == nums[j-1]) continue;
l = j + 1;
r = n - 1;
long long val = (long long) target - nums[i] - nums[j];
while(l < r){
long long sum = nums[l] + nums[r];
if(sum > val) --r;
else if(sum < val) ++l;
else{
ret.push_back({nums[i], nums[j], nums[l], nums[r]});
--r;
++l;
while(l < r && nums[l] == nums[l-1]) ++l;
while(l < r && nums[r] == nums[r+1]) --r;
}
}
}
}
return ret;
}
};
五、N数之和
给你一个由 个整数组成的数组
nums
,和一个整数 m m m以及一个目标值target
。请你找出并返回满足下述全部条件且不重复的 n n n元组 满足它们的和为target
。
1、递归+双指针
1)算法分析与描述
两数之和、三数之和、四数之和都已经解决了,那么对于这类问题,通解应该是个什么样子的呢?
在一个长度是
m
m
m的数组中寻找和为
t
a
r
g
e
t
target
target的
n
n
n元组,那我们是不是就可以先枚举第
i
i
i个数,然后在
[
i
+
1
,
m
]
[i+1,m]
[i+1,m]中寻找
n
−
1
n-1
n−1个数之和,然后将找到的
n
−
1
n-1
n−1个数与第
i
i
i个数就是答案的组合之一。
n=2
时,就是一个双指针问题,于是问题可以转化为递归问题。
2)时间复杂度
从 m m m个数中取 n n n个数,也就是 C m n C_m^n Cmn,是阶乘时间复杂度。
3) 算法
这个算法大概率超时
2、递归+双指针+后缀和剪枝
1)算法分析与描述
在
1
1
1的基础上求出数组的后缀和,并根据后缀和进行最大值和最小值剪枝操作,具体的:
(1)对当前枚举的
i
i
i,若最大的
n
−
1
n-1
n−1个数之和小于
t
a
r
g
e
t
target
target,在进行下一个数的枚举;
(2)若当前
i
i
i,与其之后的
n
−
1
n-1
n−1个数之和大于
t
a
r
g
e
t
target
target,则break
。
2)时间复杂度
从 m m m个数中取 n n n个数,也就是 C m n C_m^n Cmn,是阶乘时间复杂度,但是加上最大值和最小值剪枝,数据基本上就不会卡了。
3) 算法
class Solution{
vector<int> posSum;
public:
void comPosSum(vector<int> &nums){
int n = nums.size();
posSum.resize(n+1);
posSum[n] = 0;
for(int i = n-1; i >= 0; --i){
posSum[i] = posSum[i+1] + num[i];
}
}
vector <vector<int> > nSum(int n, vector<int> & nums, int l, int r, int target) {
vector <vector<int> > ret;
int i, j;
if(n == 1) { // 一数之和
for(i = l; i <= r; ++i) {
if(nums[i] == target) {
ret.push_back({target});
break;
}
}
return ret;
}
if(n == 2){
while(l < r){
int now = nums[l] + nums[r];
if(now > target) --r;
else if(now < target) ++l;
else{
ret.push_back({nums[l], nums[r]});
--r;
++l;
while(l < r && nums[l] == nums[l-1]) ++l;
while(l < r && nums[r] == nums[r+1]) --r;
}
}
}
for(i = l; i <= r - (n-1); ++i){
if(i > l && nums[i] == nums[i-1]) continue; // 重复的情乱
if(nums[i] + posSum(r - (n-1) + 1) < target) continue; // 最大剪枝
if(posSum[i] - posSum(i+n) > target) break; // 最小剪枝
vector<vector<int>> v = nSum(n-1, nums, i+1, r, target-nums[i])
for(j = 0; j < v.size(); ++j){
v[j].push_back(nums[i]);
ret.push_back(v[j]);
}
}
return ret;
}
};
六、总结
以上主要介绍了N数之和的知识,实则考察的是在数组中查找指定元素的知识,指定元素可以是单个元素也可以是多个。根据查找的元素的个数可以将查找方法分为线性枚举、二分查找等方法。
- 线性查找:就是遍历数组即挨个枚举数组中的元素,判断当前遍历的元素是否是需要查找的元素,若是则返回,否则遍历下一个元素对其进行判断。
- 二分查找:简明扼要的讲就是根据条件缩短查找范围,直至查找到指定元素或者遍历完整个查找范围仍未找到则返回。一些常用可以参考 二分查找 。