四数相加
我的思路是创建1个哈希表numMap1存储nums1、nums2各元素相加结果、numMap2存储nums3、nums4中各元素相加结果,由于最后只需考虑返回组合的数量而不是实际的组合列表,我用哈希表记录相加结果的次数,如假设nums1[1,2]、nums2[2,3],则哈希表numsMap1[3]=1、numsMap[4]=2、numsMap[5] = 1,同理在numsMap2中也会存储类似结构,这时,为了实现四数相加为0的结果,需要查找numsMap1和numsMap2关键字相反的数据,如numsMap1中有关键字3、4、5,我们若想四数相加为0,则需要numsMap2中寻找相反数-3、-4、-5,假设找到numsMap2[-4]=2,则最终结果count需要加上2*2,即在numsMap1中相加和为4的有2对,在numsMap2中相加和为-2的有2对,组合起来,共有4对四数相加为0。以此类推,遍历完一个哈希表就能得到四数相加为0的所有可能对数。
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
int n = nums1.size();
std::unordered_map<int,int>numMap1;
std::unordered_map<int,int>numMap2;//创建2个哈希表来分别存储第一二数组、第三四数组中元
//素相加的结果
for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++){
numMap1[nums1[i]+nums2[j]] += 1;//相加结果每相同一次,哈希表中值加1
numMap2[nums3[i]+nums4[j]] += 1;
}
}
int count = 0;//创建返回值count
for (auto&x:numMap1){
if(numMap2.count(-x.first)){//遍历哈希表1时,若发现哈希表2中存在有值与哈希表1相反
//count+=两数相乘
count += (numMap1[x.first]) * (numMap2[-x.first]);
}
}
return count;//返回count
}
};
算法的空间复杂度为O(n),时间复杂度为O(n^2)。
上述自己想的方法用了两个map导致算法的内存消耗太大。贴一下代码随想录的结果,旨在使用一次map,在第二次循环中直接查找结果,更快。
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap; //key:a+b的数值,value:a+b数值出现的次数
// 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; // 统计a+b+c+d = 0 出现的次数
// 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
赎金信
与有效的字母异位词类似,创建一个26位的数组,第一次遍历magazine,将magazine中的元素数目记录在数组中,在第二次遍历ransomNote时,每遍历一次,数组对应索引保存元素-1,当存在一个索引保存元素小于0时,说明magazine中的字母无法表示ransomNote,返回false,否则返回true。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
vector<int> ivect(26, 0);//创建26个字母的数组,代表‘a’~‘z’
for (int i = 0; i < magazine.size(); i++) {
ivect[magazine[i] - 'a']++;
}//遍历magazine,将magazine中元素对应数组的索引保存元素自加1
for (int j = 0; j < ransomNote.size(); j++) {
ivect[ransomNote[j] - 'a']--;遍历ransomNote并将元素对应数组的索引保存元素自减1
if (ivect[ransomNote[j] - 'a'] < 0) {//若存在元素小于0,则说明无法构建
return false;
}
}
return true;//否则可以构建,返回true
}
};
三数之和
哈希表
有考虑使用哈希表来解决,遍历两遍数组,将数组所有两次相加的可能结果(nums[i]+nums[j],{nums[i],nums[j]})存入一个哈希表,映射方式为键可重复的multimap,multimap<int.vector<int>>numsmap;之后再遍历一遍数组,寻找numsmap中与数组元素相反的键及其值,这里就出现了去重的问题,去重有2个,一是数组中的重,如数组如果全为0,则只需考虑一种结果,二是numsmap中的重,如考虑三个键值对{0:[-1,1]}{0:[-2,2]}和{0:[-1,1]},只需考虑{0:[-1,1]}和{0:[-2,2]}。未成功做出,贴出代码随想录的哈希表做法。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[j], c = -(a + b)
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么不可能凑成三元组
if (nums[i] > 0) {
break;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
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]) { // 三元组元素b去重
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;
}
};
算法的时间复杂度O(n^2),空间复杂度O(n)。
双指针法
已知数组nums,需找nums中的非重合元素[a,b,c]使得a + b + c = 0;
需先对数组nums进行排序,使nums有序。以for循环遍历数组,其中指示变量i以及我们创建的left = i + 1及right指针用于返回符合要求的结果。具体流程参考算法随想录哈希表下三数之和。
基本流程如下,创建结果二维数组vector<vector<int>> result,在对数组nums排序后,进行循环,若第一个元素已大于0,则直接返回结果二维数组result(排序后若第一个元素大于0,之后无论如何加都不会得到0),令left = i+1;right = nums.size()-1;当(nums[i]+nums[left]+nums[right]>0)时,令right--,这里主要是由于数组已排序,left不能向前,只能减小right,当(nums[i]+nums[left]+nums[right]<0)时,令left++,道理同上,right不能后移,只能++left;此外就是相等的情况,将vector<int>{nums[i],nums[left],nums[right]}存入result数组,此时,需要同时改变left和right数组,left++,right--。若只是这样遍历,会存在很多重复的结果,考虑题意要求的唯一答案。
考虑这样,需要对i、left和right所指向数据进行去重,第一次是对i的去重,若(i>0 and nums[i] == nums[i--])令i++,同时continue,这个在i = 0时是不会执行的,这里的条件其实主要有两种可能,分别为num[i] == num[i--]和nums[i] == nums[i++],但第二个条件会遗漏[-1,-1,2]这样的情况(因为right = i+1),所以选择(i>0 and nums[i] == nums[i--]),这次去重的位置选在判断完nums[0]>0后,针对right和left的去重选择在一次输出结果之后,如[0,-1,-1,-1,-1,1,1,1,1]这样的数组,需要去重,在对left和right更新前,将left和right与其移动位置之后的元素进行比较,利用while,判断nums[left] == nums[left+1],nums[right] == nums[right - 1],若相等,left = left + 1,right = right -1,这里使用while循环,需注意要满足前提条件right>left,之后循环完返回结果result。时间复杂度O(n^2),空间复杂度O(1)。
还有一个需要注意的点,在leetcode上,针对left和right的剪枝操作,需要left在后,right在前,否则会溢出报错。但我在本地的编译器上没有这样的问题。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
sort(nums.begin(), nums.end());
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.size(); i++) {
// 排序之后如果第一个元素已经大于零,那么无论如
//何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
// 错误去重a方法,将会漏掉-1,-1,2 这种情况
/*
if (nums[i] == nums[i + 1]) {
continue;
}
*/
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
// 去重复逻辑如果放在这里,0,0,0 的情况,
//可能直接导致 right<=left 了
//从而漏掉了 0,0,0 这种三元组
/*
while (right > left && nums[right]
== nums[right - 1]) right--;
while (right > left && nums[left]
== nums[left + 1]) left++;
*/
if (nums[i] + nums[left] + nums[right] > 0)
right--;
else if (nums[i] + nums[left] + nums[right] < 0)
left++;
else {
result.push_back(vector<int>{nums[i], nums[left], nums[right]});
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
// 找到答案时,双指针同时收缩
right--;
left++;
}
}
}
return result;
}
};
四数之和
参考上题的三数相加的双指针法,再加一层外循环。此外,需要考虑一些细节,首先,由于计算的target不是上题般恒为0,因此最开始的判断需要改变为
if(nums[i] > target && (nums[i] >=0 || target >= 0)){
break;
此外,在第一层内循环时同样也要加入这样的判断
if(nums[j] + nums[i]>target && nums[i]+ nums[j]>=0){
break;
}
也可不加,不过会减慢效率,此外,考虑到这个四数相加的结果可能超出int范围,将四数相加的结果保存为long,这里卡了我很久,如果不保存为long,leetcode大概会在280+停止。
if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
left++;
} else if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
}
具体代码如下:
class Solution {
public:
std::vector<std::vector<int>> fourSum(std::vector<int>& nums, int target) {
std::sort(nums.begin(), nums.end()); // 先对数组进行排序
std::vector<std::vector<int>> ans;
for (int i = 0; i < nums.size(); i++) {
// 跳过重复的数字
if(nums[i] > target && (nums[i] >=0 || target >= 0)){
break;
}
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
for (int j = i + 1; j < nums.size(); j++) {
// 跳过重复的数字
if(nums[j] + nums[i]>target && nums[i]+ nums[j]>=0){
break;
}
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
int left = j + 1, right = nums.size() - 1;
while (left < right) {
if ((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
left++;
} else if ((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
} else {
ans.push_back({nums[i], nums[j], nums[left], nums[right]});
// 跳过重复的数字
while (left < right && nums[left] == nums[left + 1]) {
left++;
}
while (left < right && nums[right] == nums[right - 1]) {
right--;
}
left++;
right--;
}
}
}
}
return ans;
}
};
算法的空间复杂度O(1),时间复杂度O(n^3),具体解释如下
-
排序:首先对数组进行排序,这需要 O(nlog n) 的时间。
-
遍历数组:算法中有一个外层循环,它遍历数组中的每个元素作为四数之和的一个元素,这需要 O(n) 的时间。
-
内部循环:对于每个外层循环中的元素,有一个内层循环,它也遍历数组中的每个元素作为四数之和的一个元素,这同样需要 O(n) 的时间。
-
双指针:在内层循环中,我们使用双指针来找到和为
target - nums[i] - nums[j]
的两个元素,这需要 O(n) 的时间。
因此,总的时间复杂度是 O(n log n + n^2 + n^2) = O(n^3),其中 n 是数组的长度。在最坏的情况下,每个元素都可能被用作四数之和的一个元素,导致双指针操作的次数达到 n^2。