非科班学习算法day6 | LeetCode454: 四数相加||,Leetcode383:赎金信 ,Leetcode15:三数之和,LeetCode18:四数之和
目录
介绍
包含LC的两道题目,还有相应概念的补充。
相关图解和更多版本:
代码随想录 (programmercarl.com)https://programmercarl.com/#%E6%9C%AC%E7%AB%99%E8%83%8C%E6%99%AF
一、基础概念补充:
和day5的补充一样!
二、LeetCode题目
1.LeetCode454:四数相加||
题目链接:454. 四数相加 II - 力扣(LeetCode)
题目解析
昨天理解了两数之和的思路是:先建立哈希表,然后在其中寻找target-当前元素的目标元素。那么四个数的和是不是也能用类似的思路?答案是肯定的。
首先也是利用map存储前两个数组中元素相加的结果统计:用键存储两数之和,用键值存储两数出现的次数;然后在另外两个数组元素加和的结果中统计出现次数。
c++代码如下:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4)
{
//定义map1存储前两个的遍历结果
unordered_map<int, int> m;
//对数组1和2的和进行存储
for(auto n1:nums1)
{
for(auto n2:nums2)
{
m[n1 + n2]++;
//if(m.find(n1+n2)!=m.end())
//{
// m[a+b] = m[a+b]+1;
//}
//else
//{
// m[a+b] = 1
//}
}
}
//初始化记录次数
int count = 0;
for(auto n3 : nums3)
{
for(auto n4 : nums4)
{
if(m.find(0-n3-n4) != m.end())
{
count += m[0-n3-n4];
}
}
}
return count;
}
};
注意点1:这里的写法m[n1 + n2]++;我一开始不太懂,表示的是备注中的写法。因为用加和的元素作为键,所以在map中有这个键就把键值加一,没有则创建,把键值赋予1.
//if(m.find(n1+n2)!=m.end())
//{
// m[a+b] = m[a+b]+1;
//}
//else
//{
// m[a+b] = 1
//}
注意点2: 这里用键存储和,用键值存储次数,这样操作起来更加简单合理,因为和可能多次出现,可以根据键索引到键值存储的次数直接修改,如果反过来:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4)
{
// 定义map1,但这次键是次数,值是和的集合(使用vector存储可能重复的和)
unordered_map<int, vector<int>> m;
// 对数组1和2的和进行存储,并初始化或增加对应和值的计数
for(auto n1:nums1)
{
for(auto n2:nums2)
{
int sum = n1 + n2;
auto it = m.find(sum);
if (it != m.end()) {
it->second.push_back(sum); // 相同和值,增加一个实例到vector中
} else {
m[1].push_back(sum); // 首次遇到此和值,设置次数为1
}
}
}
// 初始化记录次数
int count = 0;
for(auto n3 : nums3)
{
for(auto n4 : nums4)
{
int target = -(n3 + n4);
if(m.find(target) != m.end())
{
count += m[target].size(); // 累加该和值出现的次数
}
}
}
return count;
}
};
2.Leetcode383:赎金信
题目解析
很有电影画面的一道题,我认为这道题和349. 两个数组的交集 - 力扣(LeetCode)很相似,只不过我们需要注意的是:这里对母字符串中的元素放到子字符串中的这个操作的过程中,要求母串该元素不得少于字串该元素数量。
选用的容器还是:数组。
C++代码如下:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine)
{
//如果短,那么直接判定为假
if(ransomNote.size() > magazine.size())
{
return false;
}
//设置字符串的哈希表
int count[26] = {0};
//遍历字符串2记录
for(auto mag : magazine)
{
count[mag-'a']++;
}
//遍历字符串1检验
for(auto ran : ransomNote)
{
count[ran-'a']--;
if(count[ran -'a'] < 0)
{
return false;
}
}
return true;
}
};
注意点1:检验的条件是遍历子串,如果相同元素的数量大于母串,那么就返回false
3.Leetcode15:三数之和
题目解析
这道题我是后看的,我建议是先看18. 四数之和 - 力扣(LeetCode),其实思路是一样的。
C++代码如下:
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;
}
};
注意点1:我觉得我的下面一道题写的不如代码随想录里去重讲的好,参考一下吧
4.Leetcode18:四数之和
题目解析
这题和前面四树之和的区别在:同一个数组检索,所以要求不能重复同一下标元素,也不能在结果中返回内容完全一样的数组,那么去重就成了最大的难题。
这里虽然我也跟着写过哈希表的方法,判定非常多,而且很复杂,最后也是使用双指针的方法求解。
C++代码如下:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target)
{
//参考三数之和的做法,那么需要先循环遍历a和b
//建立存储结果的v
vector<vector<int>>result;
//对数组排序
sort(nums.begin(), nums.end());
//循环!
for(int i = 0; i < nums.size(); i++)
{
//a去重检查
if(i > 0 && nums[i] == nums[i - 1]) continue;
for(int j = i + 1; j < nums.size(); j++)
{
//b去重检查
if(j > i + 1 && nums[j] == nums[j - 1])continue;
//接下来设置双指针
int left = j + 1;
int right = nums.size() - 1;
//检查条件调整指针
while(left < right)
{
if((long)nums[i] + nums[j] + nums[left] + nums[right] > target) right--;
else if((long)nums[i] + nums[j] + nums[left] + nums[right] < target) left++;
else
{
result.push_back(vector<int>{nums[i], nums[j], nums[left], nums[right]});
//检查去重
while(right > left && nums[right] == nums[right - 1])
{
right--;
}
while(right > left && nums[left] == nums[left + 1])
{
left++;
}
//完成一轮检索,收缩指针
left++;
right--;
}
}
}
}
return result;
}
};
注意点1:比较重要的一点,直接更改原数组,对其排序,这样子的好处,就可以在后面选择元素的时候,不用把当前元素和全部元素都比较一遍,而是就比对附近的元素。也是后面移动双指针的前提!
注意点2:基本的做法还有点类似于哈希表,不过有本质区别,哈希是寻找目标值,直接可以锁定;双指针是逐步逼近调整。
将四个数先分成两组,第一组两个和用两层循环遍历,这样可以避免掉一个元素用了两次,那么同时还要有对相同元素的去重作用,因为[nums[a], nums[b], nums[c], nums[d]]按照我们排序的条件下返回必定是有序的,所以在一层循环里面就只需把相同元素用一个,
这里插入举个例子就是:排序之后的数组可能是[-2,-2,.........],那对于外层循环i来说,位置0的-2循环之后的结果,不和位置1的-2循环的结果可能有相同的么(想象两个-2换位置),所以插入了去重操作。
注意点3:双指针的逻辑并不难,难的也是去重的操作能不能想好,我一开始试了先收缩指针,再去重,这样是会报错或者有重复解。
注意点4:这里用了一个类型转换(long),因为题目的要求已经超出了int的32位的范围,所以要做强制类型转换。
总结
打卡第6天,很难吖!明天之后会有很讨厌的字符串还有KMP算法,坚持!!!