四数相加
题干
题目:给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0,即要返回元组的数量。
为了使问题简单化,四个数组具有相同的长度 N,且 0 ≤ N ≤ 500。所有整数的范围在 -2^28 到 2^28 之间,最终结果不会超过 2^31-1 。
思路
昨天做了一道题“两数之和”,用一个 map 存储遍历过的元素,在后续匹配的时候可以直接查找 map ,所以产生了把四数相加拆成两数相加再匹配的想法,即先把每两个数组的元素进行相加求和,再用 map 记录和、和的次数。同时此题不需要排序,所以可以选择 unordered_map,以达到最高的查询效率。
方法一:先用两个 for 循环存储 两个数组元素相加的和 在 两个unordered_map 中,将四数相加转化为 两数相加是否匹配 的问题。时间复杂度 O(n^2),力扣运行时间大概两百多毫秒。
方法二:看了题解,也是先两个 for 循环存储两个数组元素的和、和的次数,但题解只需要用一个 map 即可,之后在匹配时直接遍历另外两个数组。时间复杂度 O(n^2),空间复杂度 O(n^2),空间复杂度主要由 map 长度决定,map 存储两数相加的和,最坏情况下,每两个数相加的和都不同,则 map 最大长度可能达到 n^2。
对比:两个方法本质思路一样,但是方法一用了两个 map 更耗费空间。
代码
方法一:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> twoNumSum1; // 存储前两个数组的和、和的次数
unordered_map<int,int> twoNumSum2; // 存储后两个数组的和、和的次数
for (int i = 0; i < nums1.size(); ++i) {
for (int j = 0; j < nums2.size(); ++j) {
int sum1 = nums1[i] + nums2[j];
int sum2 = nums3[i] + nums4[j];
if (twoNumSum1.find(sum1) == twoNumSum1.end()){
twoNumSum1.insert({sum1,1});
} else{
twoNumSum1[sum1]++;
}
if (twoNumSum2.find(sum2) == twoNumSum2.end()){
twoNumSum2.insert({sum2,1});
} else{
twoNumSum2[sum2]++;
}
}
}
int count = 0 ; // 记录元组个数
for (auto it = twoNumSum2.begin(); it != twoNumSum2.end(); ++it) {
int item = 0-it->first;
if (twoNumSum1.find(item) != twoNumSum1.end()){
// 在另一个 map 中找到了匹配的元素
auto it2 = twoNumSum1.find(item);
// 该元组的个数是两个和的次数相乘
count += (it->second)*(it2->second);
}
}
return count;
}
};
方法二:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> record; // 记录前两个数组元素相加的和、和的次数
for (int i : nums1) {
for (int j : nums2) {
int sum = i+j;
record[sum]++;
}
}
int count = 0; // 统计元组出现的次数
for (int i : nums3) {
for (int j : nums4) {
int item = 0-i-j; // 记录需要匹配的元素
if (record.find(item) != record.end()){
// 如果在map中找到了匹配的元素,则统计元组出现的次数
// 注意,这里不是 count++
count += record[item];
}
}
}
return count;
}
};
赎金信
题干
题目:给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。两个字符串均由小写字母构成。
(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)
思路
方法一map:先用 unordered_map 存储杂志 magazine 字符串出现的字母和字母个数,在遍历赎金新 ransom 字符串时,查询是否在 map 中以及是否还有使用次数。
方法二数组:用长度为 26 的数组存储 26 个字母在 magazine 中出现的次数,之后再遍历 ransom 时,看 ransom 的字母是否在数组中次数大于 0,次数大于 0 说明该字母可以使用,否则返回 false。
用 map 和数组对比:两个时间复杂度都是O(n),但是看了题解,使用 map 的空间消耗会比数组大,当 数据量很大时,map 需要维护哈希表或红黑树,还要做哈希函数,会比数组更费时。
代码
方法一:map
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char,int> characters;
for (char c : magazine) {
characters[c]++;
}
for (char c : ransomNote) {
if (characters.find(c) == characters.end()){
return false;
} else{
if (characters[c] > 0){
characters[c]--;
} else{
return false;
}
}
}
return true;
}
};
方法二:数组
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int characterCount[26] = {0};
for (char c : magazine) {
characterCount[c-'a']++;
}
for (char c : ransomNote) {
if (characterCount[c-'a'] > 0){
characterCount[c-'a']--;
} else{
return false;
}
}
return true;
}
};
三数之和
题干
题目:给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
思考
方法一双指针法:要先将数组进行从小到大排序,一是为了缩小搜索范围,也就是当遍历到 nums[i] 已经大于0时就可以停止遍历直接返回,因为是有序的说明之后的数都大于 0,已经没有满足 “和为0” 的三元组存在了;二是排序后的数组方便去重,因为排序后相同的元素将会连续存放在数组中,如果之前该元素已经找到匹配的三元组,后续连续碰到相同元素时,则可以 continue 跳过。
如何找到和为0的三元组?在遍历数组时,可以先确定一个元素nums[i],设置两个指针 left 和 right 遍历之后的数组用于找到和 nums[i] 匹配的另外两个元素 nums[left] 和 nums[right],由于数组有序,可知左边的元素一定小于右边的,因此当这三个数之和大于 0,说明需要减小右边的数,即 right-- ;如果三数之和小于 0,说明需要增大左边的数,即 left++;当三数之和为 0,就找到了对应的三元组,将其插入结果中。
如何进行三元组去重?我们已经固定了一个元素 nums[i],一次 while循环移动 left 和 right 指针的过程就是对元素 nums[i] 进行匹配的过程,当 while 循环结束就说明包含元素 nums[i] 的三元组已经确定,后续不需要再重复对 nums[i] 进行匹配。对nums[i]的去重操作是在 for 循环遍历数组时,先判断 nums[i] 是否之前已经匹配过了,即 nums[i] 是否和 nums[ i - 1]相等。
当在 while 循环移动双指针时,找到了匹配的三元组,那么就需要对之后的 nums[left] 和 nuns[right] 进行去重,因为数组是有序的,所以重复的元素会放在一起,则只需要判断 left 指针之后的元素是否和 nums[left] 是重复的,如果是重复的那么就让 left 指针不断移动指向下一个不同的元素,即 left++;同理,只需要判断 right 指针之前的元素是否和 nums[right] 重复,若重复就让 right 指针不断左移指向下一个不同的元素,即 right--。
方法二哈希法:后续会整理出思路笔记,先只放代码
代码
方法一:双指针法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
// 让数组从小到大排序
sort(nums.begin(),nums.end()); // sort排序函数在标准库中的<algorithm>
vector<vector<int>> result; // 存储三元组
for (int i = 0; i < nums.size(); ++i) {
// 如果此元素大于 0,之后所有的数都比该元素大,则后续不可能存在 和为 0 的三元组,直接返回
if (nums[i] > 0){
break;
}
// 对 nums[i] 去重,已经匹配过的 nums[i] 直接跳过
if (i > 0 && nums[i] == nums[i-1]){
continue;
}
int left = i+1;
int right = nums.size()-1;
// 开始寻找和 nums[i] 匹配的另外两个数
// left 不可以等于 right,不然元素重复
while (left < right){
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0){
right--;
} else if (sum < 0){
left++;
} else{
// 三数之和为 0
result.push_back({nums[i],nums[left],nums[right]});
// 对 nums[left] 去重
while (left < right && nums[left] == nums[left+1]){
left++;
}
// 对 nums[right] 去重
while (left < right && nums[right] == nums[right-1]){
right--;
}
// 上述两个 while 循环找到了最后重复的 left 和 right,故双指针此时收缩一次即可
left++;
right--;
}
}
}
return result;
}
};
方法二:哈希法
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result; // 存储三元组结果
sort(nums.begin(), nums.end()); // 排序数组
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
// 对 nums[i] 去重
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
unordered_set<int> set; // 存储第三个匹配的元素
for (int j = i + 1; j < nums.size(); j++) {
if (j > i + 2
&& nums[j] == nums[j-1]
&& nums[j-1] == nums[j-2]) { // 对nums[j]去重
continue;
}
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 三元组元素c去重
} else {
set.insert(nums[j]);
}
}
}
return result;
}
};
四数之和
题干
题目:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。
注意:答案中不可以包含重复的四元组。
思路
前面用双指针法求三个数之和,那么四数之和可以用双指针法吗?三数之和是先用下标 i 确定一个值,再用双指针找到另外两个匹配的值,而四数之和是要先确定两个值,则需要再多一层 for 循环,确定两个值后,再用双指针寻找另外两个匹配的数,逻辑相通。
代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
sort(nums.begin(),nums.end());
vector<vector<int>> result;
for (int i = 0; i < nums.size(); ++i) {
// 剪枝处理
// 如果此时 nums[i]已经比target大了,而且是正数
// 那么后续相加的和肯定都大于 target,不会等于 target
if (nums[i] > target && nums[i] >= 0){
break;
}
// 对 nums[i]去重
if (i > 0 && nums[i] == nums[i-1]){
continue;
}
for (int j = i+1; j < nums.size(); ++j) {
// 二级剪枝,跟之前的剪枝处理理由相同
if (nums[i]+nums[j] > target && nums[i]+nums[j] > 0){
break;
}
// 对 nums[j]去重
if (j > i+1 && nums[j] == nums[j-1]){
continue;
}
int left = j+1;
int right = nums.size()-1;
while (left < right){
// 用 long 类型存储,如果用 int 可能会溢出
// 题干条件是数组元素 -10^9 ≤ nums[i] ≤ 10^9
// 如果四个元素都是10^9,那么 sum 就是4*10^9,超过了int的最大整数范围
long sum = (long)nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target){
right--;
} else if (sum < target){
left ++;
} else{
result.push_back({nums[i],nums[j],nums[left],nums[right]});
// 对 nums[left] 去重
while (left < right && nums[left] == nums[left+1]){
left++;
}
while (left < right && nums[right] == nums[right-1]){
right--;
}
left++;
right--;
}
}
}
}
return result;
}
};
报错记录
runtime error: signed integer overflow
这个错误信息表明在程序运行时发生了有符号整数溢出。这意味着你试图将一个超出有符号整数类型所能表示的最大范围的数值赋给一个有符号整数变量。此时考虑更换为更大的整数类型 long。