454.四数相加Ⅱ
难度指数:😀😐😕
相对于四数之和,简单在:无需考虑去重的操作。
举个例子:如果数组里面所有的元素都是0,那么我们想要寻找的四元组就是[0,0,0,0],[0,0,0,0],……,[0,0,0,0]
虽然四元组都是[0,0,0,0],但是不同的0取自于数组里的不同位置,因此这道题 无需去重
。
在一个集合里面,需要判断一个元素有没有出现过,就要用哈希法。
暴力:
4个for循环遍历4个数组,取不同元素相加,如果 = 0,count++,最后返回count。 时间复杂度O(n^4)
哈希表:
可以只遍历这2个数组,
在遍历完A和B数组的时候,可以把 a + b
的值放到一个集合里;然后在判断C、D数组的时候,判断集合里有没有我们想要的元素,如果有,说明我们找到了一个匹配项 target = 0
,这时就可以 count++
了。
⚠️这道题元素的数量可能很大(int),用数组下标来做映射,肯定不够;只能考虑 set
或者 map
。
不仅要统计 a + b
在这个集合里有没有出现过,还要统计出现过多少次,然后才能和下面的 c + d
做一个映射。
如果使用的数据结构是
set
,我们只有一个key
去存是否出现过;然而,出现过的次数,我们还需要一个value
来存,因此考虑用map
。
整个题目的解题思路:
在遍历A、B数组的时候,将 a + b
放到map集合里面的key,同时统计 a + b
出现过的次数。
然后在遍历C、D数组的时候,判断 0 - (c + d)
有没有在map集合里面出现过;如果出现过,就把出现的次数做一个统计。
说一嘴:
0 - (c + d)
=a + b
Q:为什么要先遍历A、B数组,然后再遍历C、D数组? O(n^2) + O(n^2),整体就是O(n^2)
我只先遍历A数组,然后遍历B、C、D数组不行吗?
A:不可以! 遍历后面的B、C、D,时间复杂度是O(n^3)
O(n^2) 肯定比 O(n^3) 更优。
AC代码: (核心代码模式)
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
unordered_map<int, int> umap; //key, value
//遍历A、B数组,统计两个数组元素之和,以及出现的次数,放到map中
for (int a : A) {
for (int b : B) {
umap[a + b]++;
}
}
int count = 0; //统计a + b + c + d = 0 出现的次数
for (int c : C) {
for (int d : D) {
if (umap.find(0 - (c + d)) != umap.end()) {
count += umap[0 - (c + d)];
}
}
}
return count;
}
};
383.赎金信
暴力解法:
AC代码: (核心代码模式)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
for (int i = 0; i < magazine.length(); i++) {
for (int j = 0; j < ransomNote.length(); j++) {
if(magazine[i] == ransomNote[j]) {
ransomNote.erase(ransomNote.begin() + j); //ransomNote删除这个字符
break;
}
}
}
//若ransomNote为空,说明magazine的内容可以组成ransomNote
if (ransomNote.length() == 0) {
return true;
}
return false;
}
};
哈希解法:
AC代码: (核心代码模式)
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
if (ransomNote.size() > magazine.size()) {
return false;
}
for (int i = 0; i < magazine.length(); i++) {
//通过record数据记录 magazine里各个字符出现次数
record[magazine[i] - 'a']++;
}
for (int j = 0; j < ransomNote.length(); j++) {
record[ransomNote[j] - 'a']--;
if (record[ransomNote[j] - 'a'] < 0) {
return false;
}
}
return true;
}
};
15.三数之和
难度指数:😀😕🙁
虽然可以用哈希法,但是用哈希法就整复杂了。
在一个数组中找出3个元素等于0,把这个三元组都找出来。 (⚠️三元组是去重的)
本题相对于"两数之和"的复杂就体现在:这道题需要去重。
找出3个数,就是在数组里面找出 a + b + c = 0;
可以用2层for循环去遍历:第一层确定a,第二层确定b;
想找c的话就看 0 - (a + b)
这个值是否出现在数组里面:
for (a ) {
for (b ) {
0 - (a + b)
}
}
如果有,就找到了一对相加结果为0的 a、b、c
要想寻找0 - (a + b)
这个数值是否在数组里是否出现过,就要用哈希法(用 map
做映射)。
a需要去重,b需要去重,c也需要去重。
哈希解法:
不太建议用哈希法来做这道题,因为去重的细节太多了,很难做到bug free,基本上都会遇到点小问题,也很难一次想周全。
双指针解法:
更易于理解。
使用双指针法之前,一定要对数组先进行排序。
让我们找元素值的这个三元组相加等于0,没让我们返回下标。如果让你返回下标,你在一番排序之后,这些下标就都乱了。
if (nums[i] + nums[left] + nums[right] > 0) { //若3个数相加大于0
right--; //需要把3数之和变小,让right前移
}
if (nums[i] + nums[left] + nums[right] < 0) { //若3个数相加小于0
left++; //需要把3数之和变大,让left后移
}
if (nums[i] + nums[left] + nums[right] == 0) {
加进result数组
}
深入细节:🦄去重是关键
a需要去重,b需要去重,c也需要去重。
(说人话:结果集里面有一个[1, 2, 3]了,就不能再出现[1, 2, 3]了。)
伪代码:
定义一个二维数组 result
来存放结果集。
sort(nums); //对输入的数组进行排序
//遍历数组
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > 0) return; //第一个数大于0,那不用玩了(后面不管怎么收集也不可能3个数相加等于0)
}
因为我们已经遍历了nums[i],就取了a这个数,要对a进行去重,
⚠️注意细节:
Q:
nums[i] == nums[i + 1];
和nums[i] == nums[i - 1];
选哪个?A:选择后者
AC代码: (核心代码模式)
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++) {
//排序后如果第一个元素大于0,那不用玩了
if (nums[i] > 0) {
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { //三元组元素a去重
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > 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;
}
};
18.四数之和
难度指数:😀😕🙁
四数之和,和三数之和是一个思路,都是使用双指针法,基本解法就是在三数之和的基础上再套一层for循环。
for (int k) {
for (int i) {
}
}
k和i是确定的,还是靠left
和right
不断向中间移动,寻找到合适的四元组,使之相加等于target
。
在三数之和的代码基础上,外面套上一层for循环。
for (int k) {
for (int i) {
"三叔之和"的代码 (不是原封不动地复制)
}
}
nums[k] + nums[i] + nums[left] + nums[right] = target;
还是有很多小细节的,主要细节在于剪枝和去重的部分:
剪枝操作:
如何对k进行剪枝操作?
在上一题中,如果nums[i] > 0,就直接进行剪枝的操作了。
那么,我们就会以为这道题,当nums[k] > target 就可以进行剪枝操作,其实不行!
因为target是你输入的一个数,它可以是正数,也可以是负数。
⚠️注意不要惯性思维:如果全是正数,两个数相加确实可以变得更大;但如果存在负数,那么两个数相加就有可能变得更小。
eg:[-4, -1, 0, 0] 是按从小到大排序
如果target是-5,
那么这个四元组就符合条件了。
你要是写代码:
if (nums[k] > target) {
break;
}
这样就错过上面这个结果集了。
那么明白了这一点,这道题的剪枝操作就是:
也是可以做判断
if (nums[k] > target && nums[k] > 0 && target > 0) {
break; //这样就直接在这里做了剪枝
}
这里的 target > 0 也是可以不加的,只要nums[k] > 0 就避免了负数相加的情况。
去重操作:
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
进入for (int i) { }
还要对nums[i]进行剪枝和去重操作:
剪枝:
for (int i = k + 1; i < nums.size(); i++) {
if (nums[k] + nums[i] > target && nums[k] + nums[i] > 0 && target > 0) {
break;
}
}
去重:
if (int i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
AC代码: (核心代码模式)
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] > target && nums[k] >= 0) {
break;
}
//对nums[k]进行去重
if (k > 0 && nums[k] == nums[k - 1]) {
continue;
}
for (int i = k + 1; i < nums.size(); i++) {
//2级剪枝处理
if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) {
break;
}
//对nums[i]进行2级去重
if (i > k + 1 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1;
int right = nums.size() - 1;
while (right > left) {
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]});
//对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] ==nums[left + 1]) left++;
//找到答案时,双指针同时收缩
right--;
left++;
}
}
}
}
return result;
}
};