454.四数相加II
这道题比“四数之和”简单的一个地方在于,从四元组中找到的组合不需要去重,找到多少个算多少个。
如果四个元组分别为ABCD,整体思路是,把AB遍历完,将a+b的值放到一个集合里,然后再遍历CD时,去判断集合中有没有我们需要的-(c+d)使得四数之和为0。因此确定本题采用哈希法。
本题数组下标来做映射的话,占用存储空间太大,所以排除数组。又因为需要统计符合条件的组合的个数,所以,此处采用map中的unordered_map
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> mymap;
int count = 0;
for(int a:nums1){
for(int b:nums2){
mymap[a+b]++;
}
}
for(int c:nums3){
for(int d:nums4){
int t = -(c+d);
if(mymap.find(t)!=mymap.end()){
count+=mymap[t];
}
}
}
return count;
}
};
有一个要注意的是,在找到符合条件的abcd后不是用count++计数,而是count+=mymap[-(c+d)],因为找到一个-(c+d)对应的a+b的组合有几个就应该计几个。
383. 赎金信
本题判断 ransomNote 能不能由 magazine 里面的字符构成。magazine 中的每个字符只能在 ransomNote 中使用一次。这道题其实和有效的字母异位词非常相似,只需要稍稍改动一下:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int hash[26];
for(int i=0;i<26;i++){
hash[i]=0;
}
for(int i=0;i<magazine.size();i++){
hash[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.size();i++){
hash[ransomNote[i]-'a']--;
}
for(int i=0;i<26;i++){
if(hash[i]<0) return false;
}
return true;
}
};
将最后一个for循环的判断if条件改为hash【i】<0即可,即,判断ransomNote使用magazine中的字母后有没有多用超额。
简化代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int hash[26]={};
for(int i=0;i<magazine.size();i++){
hash[magazine[i]-'a']++;
}
for(int i=0;i<ransomNote.size();i++){
hash[ransomNote[i]-'a']--;
if(hash[ransomNote[i]-'a']<0) return false;
}
return true;
}
};
15. 三数之和
如果用计算四数相加的方法同样计算三数之和的话,就无法考虑到本题要求的去重操作。
使用哈希法的话,去重的细节非常繁琐。
此处采用代码随想录 中提到多次的双指针法:
-1 | -1 | 0 | 1 | 1 |
i | left | right | ||
a | b | c |
要求a+b+c=0 。
大致思路
先让数组从小到大排序。for循环遍历i,指代三数中最小的那个数a。
另外两个数,则用left和right指代,left = i+1,right = 最后一个数。
通过这样的指针分配方式,我们可以知道:
当这三数之和大于0时,需要缩小,right左移;当小于0时,需要增大,left右移。
当正好等于0时,就可以把三个数放入结果集。由nums[i]
、nums[left]
和nums[right]
组成的一维vector
添加到result
这个二维vector
的末尾。(易错点,误写成result.push_back(vector<vector<int>>{nums[i],nums[left],nums[right]}); )
去重细节
1. 剪枝
如果a作为最小的数,已经大于0了,那么无论如何三数之和都不可能等于0了。直接返回result。(注意:a可以等于0,例如0,0,0三数之和)
2. 对a去重
对a进行去重就是使a[i]!=a[i-1]。
为什么是和a[i-1]进行比较呢?
因为如果对a[i] = a[i+1] 就进行跳过的话,会排除掉{-1,-1,2}这种前两个数一致的情况。
注意:if(nums[i]!=nums[i-1]{........}不能这样写去重。因为i = 0时,i-1会造成数组越界,所以要加上i>0的条件:
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
3. 对b,c去重
while(left<right&&nums[left]==nums[left+1]) left++;//b去重 while(left<right&&nums[right]==nums[right-1]) right--;//c去重
不管b和c怎么挪,while循环里都别忘了加上left<right;
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) return result;//剪枝
// 正确去重a方法
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i+1;
int right = nums.size()-1;
while(left<right){
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]});
while(left<right&&nums[left]==nums[left+1]) left++;//b去重
while(left<right&&nums[right]==nums[right-1]) right--;//c去重
//找到结果两边都需要移动
left++;
right--;
}
}
}
return result;
}
};
ps:以上注意的点都是写完本道题第二天再写时忽略错误的点。
18. 四数之和
四数之和,和三数之和是一个思路,都是使用双指针法, 只是在三叔之和的基础上再套一层for循环。
如果充分理解了三数之和的算法逻辑,会发现四数之和这道题的步骤简直和“三数之和”一模一样。
a b c d 这四个数,在已有的b c d代码上再套一个遍历a的for循环,在遍历a和b时都是先剪枝再去重。a b c d 对应k i left right
但是由于本题求解的是=target而不是=0,得考虑target<0的情况,不能直接像上题一样通过nums[k]>target来剪枝。想剪枝排除不可能的情况必须同时满足:
if (nums[k] > target && nums[k] >= 0) {
break; // 这里使用break,统一通过最后的return返回
}
再对b进行二级剪枝时也是如此:
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
完整代码:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> result;
sort(nums.begin(),nums.end());
for(int k=0;k<nums.size();k++){
if(nums[k]>=0&&nums[k]>target){//第一个数就已经大于target了,不可能符合条件了,直接返回
break;
}
if(k>0&&nums[k]==nums[k-1]) continue;//给a去重
for(int i=k+1;i<nums.size();i++){
if(nums[i]+nums[k]>=0&&nums[k]+nums[i]>target) break;
if(i>k+1&&nums[i]==nums[i-1]) continue;//给b去重
int left = i+1;
int right = nums.size()-1;
while(left<right){
if((long)nums[k]+nums[i]+nums[left]+nums[right]>target) right--;
else if((long)nums[k]+nums[i]+nums[left]+nums[right]<target) left++;
else{
result.push_back(vector<int>{nums[k],nums[i],nums[left],nums[right]});
while(left<right&&nums[left]==nums[left+1]) left++;//给c去重
while(left<right&&nums[right]==nums[right-1]) right--;//给d去重
left++;
right--;
}
}
}
}
return result;
}
};
感想
昨天第一次做这些题时只有第二题是自己做出来的,当时觉得“三数之和”与“四数之和”都很难,第二天再做肯定忘了大部分了。结果没想到,今天做三数之和时几乎都想起来了,只有少数几个细节没有注意到(已经加粗加深理解了)。四数之和昨天没理清的剪枝去重操作今天再做,也可以很顺利地和“三数之和”里的对应上了。
继续加油!