一.四数相加
1.题目描述
给你四个整数数组 nums1
、nums2
、nums3
和 nums4
,数组长度都是 n
,请你计算有多少个元组 (i, j, k, l)
能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
示例 1:
输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2] 输出:2 解释: 两个元组如下: 1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0 2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0
示例 2:
输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0] 输出:1
2.思路分析
本题乍一看和之前的两数之和相似,但两数之和利用哈希法好求解,此题再利用哈希就不好解了。因为哈希法在不超时的情况下还要去重是很难的,(虽然网站提供了相应解法,但对于我这种小白来说,一刷还是乖乖按大佬的步子来吧)
所以,本题采用普通循环来做:
1)定义 unordered_map 用来存放 A 和 B 数组中两两数结合所求得的 和值 以及这个和值 出现的次数。
2)遍历 C 、D 数组,找是否有 (0 - c - d)的值出现在 上述的map容器中,若有,则说明 0 - c - d = a + b,将c 、d 移过来 ,就得到了 a + b + c + d = 0,此时,只要记录了 事先存好的 a+b 出现的次数就好了。(因为本题不要求去重,所以 a+b 出现的次数就是最终符合条件的元组数)
附代码:
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map <int, int> ret;
int count = 0;
for(int a : nums1) {
for(int b : nums2) {
ret[a+b]++;
}
} //遍历并记录所有 a+b 的值
for(int c : nums3) {
for(int d : nums4) {
int target = 0 - c - d;
if(ret.find(target) != ret.end()) {
count += ret[target];
}
}
} return count;
}
};
注意:
(1)为什么是 A + B 两个两个相加和 C + D 两个两个相加的和作比较而不是 A 和B+C+D这样的比较方式呢?
因为两个两个相加,其就是一个大循环套一个小循环,时间复杂度就是 n^2,两个比较,总程序的时间复杂度就是 2n^2 ------> n^2;如果是一个三个的计算,其总时间复杂度就是 n+n^3 ------> n^3故从时间复杂度来看,两个两个更优。
(2)count 每次加的值要注意不是1.
因为题目不要求去重,所以 a+b 的值和 c+d 匹配了,就是一个符合条件的元组,那么 map 中存放了几个这样的 a+b ,就有几个符合条件的元组,所以,count 每次增加的值就是 符合条件的 在map中存放着的个数。
注意: 定义 map 的时候不要忘记给初值 0 !!!
(3)在 C++中,用于遍历数组和容器的简易写法:
for(类型 变量名 : 数组或容器)
只适用于C++且只适用于数组 / 容器!!!
二.赎金信
1.题目描述
给你两个字符串:ransomNote
和 magazine
,判断 ransomNote
能不能由 magazine
里面的字符构成。
如果可以,返回 true
;否则返回 false
。
magazine
中的每个字符只能在 ransomNote
中使用一次。
示例 1:
输入:ransomNote = "a", magazine = "b" 输出:false
示例 2:
输入:ransomNote = "aa", magazine = "ab" 输出:false
示例 3:
输入:ransomNote = "aa", magazine = "aab" 输出:true
2.思路分析
本题和字母异位词极为相近!大致代码也相同
不过要注意以下几点:
(1)字母异位词不管是谁放入容器中进行字母数字统计判断都可以,但本题只能是 maganize 放入容器中。
(2)添加了新的前置条件:当 ransomNote 的长度大于 magazine 时,2 肯定组不成 1 了,直接返回即可。
附代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int result[26] = {0};
if(ransomNote.size() > magazine.size())
return false;
for(int i = 0; i < magazine.size(); i++) {
result[magazine[i] - 'a']++;
}
for(int i = 0; i < ransomNote.size(); i++) {
result[ransomNote[i] - 'a']--;
}
for(int i = 0; i < 26; i++) {
if(result[i] < 0) //小于0,说明ma中字母不足够ran使用
return false;
}
return true;
}
};
三.三数之和
1.题目描述
给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。
注意: 答案中不可以包含重复的三元组。
示例:
给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
2.思路分析
本题再使用哈希法就不合适了,因为哈希法去重真的很困难!(因为哈希找某一数值时是无序查找的,所以难免会有重复的时侯)
所以本题采用双指针的求法:
(1)大致思路是,先确定一个数值 a,然后利用两个指针去寻找 符合条件的 b 、c 的值。
(2)不管是 a 还是找 b 、c,如果这个集合是无序的,那么在找到一组 b、c 时,如果三者相加和 大于 0 或者小于 0 ,指针是无法进行我们想要的移动的:例如让 b 小一点或让 c 大一点。所以为了方便,我们要先对集合进行排序。
(3)排序我们采用的是 从小到大 的次序,而不用 从大到小 的次序。例如数组全是正数,我们本可以利用 小到大的 次序在初始时判断首值 >0, 就说明这个数组无论再怎么相加也不会得到 0 ,这种情况就直接返回退出了,不必再跑多余的循环;而利用 大到小 的次序,就必须全部跑完一遍才行。
(4)题目要求的是 不能重复的三元组,但是每个元组内的 元素 是可以重复的。例如 元组 { -1,-1,2} ,不能因为其内有两个相同的 -1 就说他不符合条件。但是不能出现 {-1,-1,2 } 和
{ -1,-1,2}的情况(因为有的时候,第一个 -1 在数组中取值位置并不同,例如第一个元组中 a 的位置在数组中的第 0 位,而第二个元组中 a 的位置在数组中的 第 1 位,不能因为这个就说他俩是不同的)
(5)本题重点:去重
分为两种:
1)对 a 进行去重:
假设已排好的数组是: -4 -1 -1 0 1 2
大致思路是:a 依次取到这个数组的每一个值,b 是 a 的下一位,c 是从数组最后开始往前遍历。
例如,a 取 -4 时,b、c 将其余数值全部遍历完后未找到 相应的能使 a+b+c = 0 的值,所以 a 依次取到下一个值 -1。 当 a = -1 时,b = -1,c = 2,此时刚好符合条件,将这组数据存放到容器中,之后,a 不变,b 、 c 继续遍历,看剩余数值中是否还有符合条件的值。则 b = 0,c = 1,也符合条件,存放。此时已没有剩余数据,故退出此次循环,进入下一个循环中。
但 由于 a 的下一个取值 是 -1,a 作为 -1 的全部能符合条件的 b 、 c 已经遍历过且保存过了,此时如果再进行一次访问,那么最后结果势必会重复。所以,a 应该跳过此次循环,进入在下一个循环。
那么此思路如何提现到代码中呢?
我们直到,去重的本质就是找相同值,在数值中,无非就是 num[ i ] == num[ i+1 ] 或者 num[ i ] == num[ i - 1 ] 罢了。那么究竟该写哪种呢?
还是根据上面的思路来找。
如果是第一种,那么 a 在取第一个 -1 时,由于进行了 此值和下一个值 的判断,二者相等,便会跳过 这个值,就是说,最后的结果中 ,连第一个 取 -1 符合条件的值都没了。也就是没了{-1,-1,2}这个元组,那么结果自然是不对的。
那么就是第二种了。第二种写法的思路是,当 a 取到第二个 -1 时,由于他会进行此值与前一个值的比较判断,发现二者值相等,也就是说,不用再进行此次循环了,直接跳过。这样既保留了第一个 -1 ,又去重了 第二个 -1.
2)对 b、c 的去重
若集合是这样: 0 -1 -1 -1 -1 1 1 1 1
在 a = 0 时,b = -1,c = 1,这个元组就是符合条件的,而之后的 b、c 值都相同,是不需要再遍历的,所以此时要对他们去重。
这里要注意两点:
1.条件要使用 while 而不是 if,像上述集合的情况,如果使用的是 if,那么只会去重一次,后面的重复数值还是没被去掉,还是会得到重复答案
2.去重代码的位置
不能一进入大循环找 b、c 的开始就去重,这样的话,连第一个 {0,-1,1}都不会得到了,因为第一个 -1 和 1 都被去重掉了。
附代码:
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> ret; //存放最后结果的容器
sort(nums.begin(), nums.end()); //对数组进行排序:sort默认从小到大
for(int i = 0; i < nums.size(); i++) {
if(nums[i] > 0) { //判断首元素是否大于0,是则直接退出
return ret;
}
if(i > 0 && nums[i] == nums[i-1])
continue; //对a进行去重
int left = i + 1;
int right = nums.size()-1;
while(left < right) {
if(nums[i] + nums[left] + nums[right] > 0 ) right--; //大于0,c调小一点
else if(nums[i] + nums[left] + nums[right] < 0 ) left++;
else {
ret.push_back(vector<int>{nums[i], 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 ret;
}
};
注意:
(1)对 a 去重时,因为用的有 num[ i -1] , 所以我们要保证 i > 0 ,否则 i - 1 可能 < 0,导致越界访问出错。
(2)while 循环条件为什么是 left < right 而不是 left <= right。
因为如果相等,两个指针指向同一个数,题目要找的是三元数组,这样只能找到两个元素,自然就不对了。
(3)在 while 循环里,有 三个 if 语句,不能写成 : if ,if , else if 。因为本质上我们想让他访问完第一个 if 后,在访问第二个 if ,然后在访问第三个 if ,而这样写,因为 if 和 else 是成对出现的,在访问完第一个 if 后,他就直接去找与他配对的 第三个 if 前的 else 而直接跳过 第二个 if 了。
(这个错误真的很细很难发现!!!(对我来说)
四.四数之和
1.题目描述
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
https://leetcode.cn/problems/4sum/
2.思路分析
本题和三数之和的解题思路大致相同,具体有以下几点不同:
(1)剪枝不同
三数之和的剪枝,也就是对 首元素 起始的判断,是利用 : num[ i ] > 0 剪枝的,本题不能这么剪枝,原因是,target 值不确定。如果 target 值为 -5,集合是 -4,-1,0 时,首元素大于 target 就直接剪枝了,不出意外的话就出意外了。
本题的剪枝是: num[ k ] > 0 & target > 0 & num[ k ] > target
(2)与三数不同的是,还要有一个外循环 ,k 代表 第一个数,a ,b,c 分别代表剩余三个数,在求解过程中,为了方便求 b,c 的值,可以将 k+a 作为一个整体,就类似于上题中 的 a 值。
剩余注意事项和上题大差不差。
(3)判断的时候,注意四值相加可能会溢出,要强制转换成 long 型
附代码:
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> ret;
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size(); k++) {
if(nums[k] >= 0 && nums[k] > target) { //一级剪枝
break;
}
if(k > 0 && nums[k] == nums[k-1]) { //一级去重: 对k去重
continue;
}
for(int i = k + 1; i < nums.size(); i++) {
if(nums[k] + nums[i] >= 0 && nums[k] + nums[i] >target) { //将 k+a 作为一个整体,二级剪枝
break;
}
if(i > k + 1 && nums[i] == nums[i-1]) { //对a去重
continue;
}
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 {
ret.push_back(vector<int>{nums[k], nums[i], 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 ret;
}
};
今日学习真的学的我头昏眼花,前两个题还好,三数相加和四数相加真的是!梦破碎的地方!细节真的太多了太杂了!稍微一不注意就会犯错,而且不容易注意到的地方也很多!
但是弄清楚又会有莫大的快乐,今天是梦不断破碎而我又不断捡起来拼好的一天。
我需要学习的地方还很多,继续加油吧!
(终于结束今天的学习了啊啊啊、啊啊啊、啊啊啊、已发疯...)