N数之和小记

前言

参考文章-夜深人静

一、一数之和

    返回指定元素在数组中的位置,如果不存在则返回 -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;
 	}
};

二、二数之和

LeetCode 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 {};
    } 
};

三、三数之和

LeetCode 15、三数之和

给你一个包含 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;
    }
     
};

四、四数之和

LeetCode 18、四数之和

给你一个由 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 n1个数之和,然后将找到的 n − 1 n-1 n1个数与第 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 n1个数之和小于 t a r g e t target target,在进行下一个数的枚举;
    (2)若当前 i i i,与其之后的 n − 1 n-1 n1个数之和大于 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数之和的知识,实则考察的是在数组中查找指定元素的知识,指定元素可以是单个元素也可以是多个。根据查找的元素的个数可以将查找方法分为线性枚举、二分查找等方法。

  • 线性查找:就是遍历数组即挨个枚举数组中的元素,判断当前遍历的元素是否是需要查找的元素,若是则返回,否则遍历下一个元素对其进行判断。
  • 二分查找:简明扼要的讲就是根据条件缩短查找范围,直至查找到指定元素或者遍历完整个查找范围仍未找到则返回。一些常用可以参考 二分查找
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wang_nn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值