【LeetCode】题解 - nSumTarget 问题

从帅张星球,认识的 labuladong ,东哥曾带领着我们一众近 300 人,进行了为期 21 天的算法挑战,虽艰难,但始终坚持下来了(毕竟是立了军令状,而且与有关)。最近开始坚持系统学算法,重新学习了 labuladong 的 nSumTarget 模板,很受启发,尤其是模板思维。

无论是最近组织【九日集训】的英雄哪里出来,还是曾经 21 天算法挑战营的主理人 labuladong,都是前辈巨人。总能给我们的成长以加持,也总希望算法这条路,后来者居上~ 。

本文,是我学习 labuladong 公众号文章 - 《一个函数秒杀 2Sum 3Sum 4Sum 问题》之后,根据自己的理解整理而来,大家可以直接点击链接中的文章,去学习,相信看完都会忍不住 加关注的。


1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

数据提示:

  • 2 <= nums.length <= 104
  • -10^9 <= nums[i] <= 10^9
  • -10^9 <= target <= 10^9
  • 只会存在一个有效答案

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?


解法1:双层循环 O(N^2)

略。

解法2:排序 + 左右指针 O(N) + O(LogN)

使用映射或两个数组,将索引和元素值一起排序,保证索引和元素值的对应关系和顺序,元素值数组 - 左右指针,查找和为 target 的元素值对,返回下标,在索引值数组中,找到初始时,对应下标,返回即可。

解法3:查找表 - 哈希表 O(N)

  • 在哈希表中查找 target - nums[i],存在就返回索引对,不存在就放入
class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) { 
        // 构建 record 
        unordered_map<int, int> record;
        for(int i = 0; i < nums.size(); i++) {
            auto it = record.find(target - nums[i]); // 记录当前值
            if(it != record.end()) {   // 找到就直接返回
                return {it->second, i};
            }
            record[nums[i]] = i;
        }
        return {};
    }
};

167. 两数之和 II - 输入有序数组

给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列(升序) ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1]numbers[index2] ,则 1 <= index1 < index2 <= numbers.length

以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1index2

每个输入只对应唯一的答案 ,而且不可以重复使用相同的元素。解决方案必须只使用常量级的额外空间

示例 1:

输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:27 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2]

示例 2:

输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:24 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3]

数据提示:

  • 2 <= numbers.length <= 3 * 10^4
  • -1000 <= numbers[i] <= 1000
  • -1000 <= target <= 1000
  • 只会存在一个有效答案

前文中 1. 两数之和 的解法 1、3 依然有效。但 不满足 常量级辅助空间 的要求。唯解法 2 可满足,考虑到本题中数组有序答案唯一,直接上指针对撞即可,不用考虑诸如 多个解、重复解 等问题。

class Solution {
public:
    vector<int> twoSum(vector<int>& numbers, int target) {
        // 暴力枚举,TIMEOUT →  未充分利用数组有序的特点
        // 解法2:Two pointers O(N)
        // 注意:返回结果中,索引是从 1 开始的
        int N = numbers.size();

        int lo = 0, hi = N -1;
        while(lo < hi) {
            int sum = numbers[lo] + numbers[hi];
            if(sum > target) {
                hi --;
            } else if(sum < target) {
                lo ++;
            } else {
                return {lo + 1, hi + 1};
            }
        }
        throw std::invalid_argument("The input has no solution");
    }
};

按照 labuladong 文章的递进顺序,魔改本题:nums 无序,满足和为 target 的解有多个,返回所有的解。并为了向后兼容 三数之和,新的 twoSum 函数签名如下:

vector<vector<int>> twoSumTarget(vector<int>& nums, int startIndex, int target)

完整代码如下:

// 数组传入之前 排序,避免后续 三数之和 问题,在循环中,调用本方法,导致 n 次排序操作
vector<vector<int>> twoSumTarget(vector<int>& nums, int startIndex, int target) {

    int lo = startIndex, hi = nums.size() - 1;
    vector<vector<int>> res;    // 保存所有和为 target 的元素对
    while(lo < hi) {
        int left = nums[lo], right = nums[hi];
        int sum = left + right;
        if(sum < target) {
            // lo ++。同时跳过所有重复的 nums[lo]
            while(lo < hi && nums[lo] == left) lo ++;
        } else if(sum > target) {
            // hi --。同时跳过所有重复的 nums[hi]
            while(lo < hi && nums[hi] == right) hi --;
        } else {    // 元素对保存
            res.push_back({left, right});
            while(lo < hi && nums[lo] == left) lo ++;
            while(lo < hi && nums[hi] == right) hi --;
        }
    }
    return res;
}

那么,167. 两数之和 II 的解法为:

class Solution {
public:
	vector<vector<int>> twoSumTarget(vector<int>& nums, int startIndex, int target) {
	    int lo = startIndex, hi = nums.size() - 1;
	    vector<vector<int>> res;    // 保存所有和为 target 的元素对
	    while(lo < hi) {
	    	// code ...
	    }
	    return res;
	}

    vector<int> twoSum(vector<int>& numbers, int target) {
        // nums 有序,解唯一
        return twoSumTarget(nums, 0, target)[0];
    }
};

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]

示例 2:

输入:nums = [0]
输出:[]

数据提示:

  • 0 <= nums.length <= 3000
  • -10^5 <= nums[i] <= 10^5

话接上文,将 三数之和问题,拆解成寻找第一个数 nums[i] 和 剩余两数之和 ltarget - nums[i]解 的问题。此题 target 为 0,我们保留 target 参数,让题解更通用。

// 数组传入之前 排序,避免枚举第一个数时,多次调用本方法,导致 n 次排序操作
vector<vector<int>> threeSumTarget(vector<int>& nums, int startIndex, int target) {
    int N = nums.size();
    vector<vector<int>> res;

    // 穷举 threeSum 的第一个数
    for(int i = startIndex; i < N; i++) {
        // 对 target - nums[i] 计算 twoSum
        vector<vector<int>> tuples = twoSumTarget(nums, i + 1, target - nums[i]);
        for(auto& tup : tuples) {
            tup.push_back(nums[i]);     // 将满足的 nums[i] push_back 构成 三元组
            res.push_back(tup);			// 将三元组 push_back 到 res 中
        }
        // 跳过第一个元素的重复
        while(i < N - 1 && nums[i] == nums[i+1]) i ++;
    }   
    return res;
}

那么,三数之和问题的解,如下:

class Solution {
public:
	vector<vector<int>> twoSumTarget(vector<int>& nums, int startIndex, int target) {
	    int lo = startIndex, hi = nums.size() - 1;
	    vector<vector<int>> res;    // 保存所有和为 target 的元素对
	    while(lo < hi) {
	    	// code ...
	    }
	    return res;
	}
	
	// 数组传入之前 排序,避免枚举第一个数时,多次调用本方法,导致 n 次排序操作
	vector<vector<int>> threeSumTarget(vector<int>& nums, int startIndex, int target) {
	    int N = nums.size();
	    vector<vector<int>> res;

	    // 穷举 threeSum 的第一个数
	    for(int i = startIndex; i < N; i++) {
	    	// code ...
	    }
	    return res;
	}
	
	vector<vector<int>> threeSum(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        return threeSumTarget(nums, 0, 0);
    }
};

18. 四数之和

将 四数之和问题,拆解成寻找第一个数 nums[i] 和 剩余三数之和 ltarget - nums[i]解 的问题。
将 三数之和问题,拆解成寻找第一个数 nums[i] 和 剩余两数之和 ltarget - nums[i]解 的问题。
两数之和问题,已求解。

OK,见到递归,归纳如下:

  1. (n-1)Sum 问题已求解时,nSum 问题拆解成寻找第一个数 nums[i] 和 剩余(n-1) 数之和 target - nums[i] 解 的问题;
  2. n = 2 时,问题已求解。

nSumTarget 函数签名如下:
vector<vector<int>> nSumTarget(vector<int>& nums, int n, int startIndex, int target)

  • nums:已排序的数组;
  • n: 组成和的数字个数。fourSum,n 为 4;
  • startIndex:左侧索引位(左右指针 lo 的初始值);
  • target:和;

完整代码如下:

vector<vector<int>> nSumTarget(vector<int>& nums, int n, int startIndex, int target) {
    // 将 nums 排序的操作 放在外面,避免寻找第一个数字之时,n 次的 sort 操作
    assert(is_sorted(nums.begin(), nums.end()));

    int sz = nums.size();
    vector<vector<int>> res;

    // 如果组成 target 的元素个数 n 小于 2 或者 nums 元素不足 n 个,直接返回
    if(n < 2 || sz < n) return res;

    if(n == 2) { // 递归的归处,最小待解决的问题 base case → twoSum 
        int lo = startIndex, hi = sz - 1;
        while(lo < hi) {
            int left = nums[lo], right = nums[hi];
            int sum = left + right;

            if(sum > target) {
                while(lo < hi && nums[hi] == right) hi --;
            } else if(sum < target) {
                while(lo < hi && nums[lo] == left) lo ++;
            } else {    // 满足的数对,添加到 res 中
                res.push_back( {left, right} );
                while(lo < hi && nums[hi] == right) hi --;
                while(lo < hi && nums[lo] == left) lo ++;
            }
        }
    } else {    // n > 2时,返回 (n-1)Sum 的结果
        for(int i = startIndex; i < sz; i++) {
            vector<vector<int>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
            for(auto& arr : sub) {
                arr.push_back(nums[i]);
                res.push_back(arr);
            }
            // nums[i] 重复问题
            while(i < sz - 1 && nums[i] == nums[i+1]) i++;
        }
    }
    return res;
}

由此,四数之和问题的解,如下:

class Solution {
public:
	// 传入的 nums 必须有序
	vector<vector<int>> nSumTarget(vector<int>& nums, int n, int startIndex, int target) {
		// code ...
	}

    vector<vector<int>> fourSum(vector<int>& nums, int target) {
        // 数组排序
        sort(nums.begin(), nums.end());
        return nSumTarget(nums, 4, 0, target);
    }
};

参考文章

以上是我学习后的梳理,欢迎大家阅读 labuladong 公众号原文 - 《一个函数秒杀 2Sum 3Sum 4Sum 问题》。

文章信息量可能比较大,一定要对 两数之和 - 左右指针 问题有所了解之后,亲自下场写文章中的代码,且加上注释,才能有很好的理解。

当然,这篇文章一开始也是在我收藏夹中吃灰了很久,才重新阅读的。重新出发,要感谢 英雄哪里出来 的九日集训。让我坚持下来~

为其 16 轮的【九日集训】已经结束了,英雄大哥带着大家继续出发的地方在这儿,感兴趣的看一眼。

英雄哪里出来 - 自律学习群,一下午没看,就会 500 + 信息。让自律更卷。

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

老坛算粉

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

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

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

打赏作者

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

抵扣说明:

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

余额充值