1、使用map数据结构作为哈希表
1.1 leetcode 454: 四数求和(注意与leetcode 15, 18区别)
第一遍代码,Time Limit Exceeded
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
//分两组,每组n^2个,两个数组自由组合,其中一个数组放在set里被找(哈希表)(思路没问题)
int num = 0;//统计元组个数
//multiset.find 函数返回的是等价的几个元素中最先插入进容器的那个
multiset<int> hash;
for(int i = 0; i < nums1.size(); i++) {
for(int j = 0; j < nums2.size(); j++) {
hash.insert(nums1[i] + nums2[j]);
}
}
for(int i = 0; i < nums3.size(); i++) {
for(int j = 0; j < nums4.size(); j++) {
auto iter = hash.find(-nums3[i] - nums4[j]);
if(iter != hash.end()) {
for(auto k = iter; *k == *iter; k++) {//for第二个条件为满足条件的循环,使用map后这个循环可去
num++;
}
}
}
}
return num;
}
};
本题是使用哈希法的经典题目,而leetcode 15,leetcode 18并不合适使用哈希法,因为leetcode 15,18这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理
而这道题目是四个独立的数组,
只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况
对于超时问题,因为在寻找重复元素的时候进行了一个小循环,把这个小循环去掉可以降低复杂度,可以用map记录对应数字(key)重复次数(value)代替set,
for(auto k = iter; *k == *iter; k++) {//for第二个条件为满足条件的循环,使用map后这个循环可去
num++;
}
因为只需要返回个数而非具体数字组合
具体思路与第一次代码一致
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
int num = 0;
unordered_map<int, int> hash;
for(int i = 0; i < nums1.size(); i++) {
for(int j = 0; j < nums2.size(); j++) {
if(hash.find(nums1[i] + nums2[j]) == hash.end()) {
hash.insert(pair<int, int>(nums1[i] + nums2[j], 1));
}
else {
hash.find(nums1[i] + nums2[j])->second++;
}
}
}
for(int i = 0; i < nums3.size(); i++) {
for(int j = 0; j < nums4.size(); j++) {
if(hash.find(- nums3[i] - nums4[j]) != hash.end()) {
num += hash.find(- nums3[i] - nums4[j])->second;
}
}
}
return num;
}
};
可以直接使用数组下标的方法向unordered_map里面添加以及获取目标位置的元素(仅对unordered_map,multimap不成立)
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]++;//类数组添加元素,只有unordered_map有这种[a+b]引用,multimap没有
}
}
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;
}
};
时间复杂度: O(n2)
空间复杂度: O(n2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n2
1.2 leetcode 383(与leetcode 242相似)
第一遍代码:
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
unordered_map<char, int> hash;
for(char c : magazine) {
if(hash.find(c) == hash.end()) {
//判断 ransomNote 能不能由 magazine 里面的字符构成,把谁放进hash表想清楚
hash.insert(pair<char, int>(c, 1));
}
else {
hash.find(c)->second++;
}
}
for(char c : ransomNote) {
if(hash.find(c) == hash.end()) {
return false;
}
else {
hash.find(c)->second--;
}
}
for(auto iter = hash.begin(); iter != hash.end(); iter++) {
if(iter->second < 0) {
return false;
}
}
return true;
}
};
同样思路,使用下标运算实现
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
if (ransomNote.size() > magazine.size())
return false;
unordered_map<char, int> umap;
for (char c : magazine) {
umap[c]++;
}
for (char c : ransomNote) {
if (umap.find(c) == umap.end())
return false;
else {
umap[c]--;
if (umap[c] == 0)
umap.erase(c);
}
}
return true;
}
};
跟leetcode 242很像
因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组来记录magazine里字母出现的次数
然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母
依然是数组在哈希法中的应用
用map,在本题的情况下,空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的,数据量大的话就能体现出来差别了。所以数组更加简单直接有效
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int count[26] = {0};
for(char c : magazine) {
count[c - 'a']++;
}
for(char c : ransomNote) {
count[c - 'a']--;
}
for(int i = 0; i < 26; i++) {
if(count[i] < 0) {
return false;
}
}
return true;
}
};
2、双指针求和
2.1 与哈希表求和(见1.1)使用场景的不同
1、在一个数组中取元素而非多个独立的数组
2、求非重复的具体数组序列而非个数
2.2 leetcode 15:使用哈希表,超时报错
第一遍思路代码
哈希法没想到在时间内解决的办法,但是使用哈希找的时候要注意去重复元素,重复元素包括两个方面:
1、元素下标不能重复,因为在同一个数组中选,不是在三个独立的数组中,所以下一轮循环要从 下一个元素开始
2、也难以保证三个数字的组合不重复(因为有相同的元素重复出现)
所以要从两个方向去重
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
//[[-1,0,1],[-1,2,-1],[0,1,-1],[2,-1,-1]],正确的[[-1,-1,2],[-1,0,1]]如果不排序没办法去重第一种情况
//暴力法
multiset<int> n(nums.begin(), nums.end());
vector<int> nn(n.begin(), n.end());
for(int i = 0; i < nums.size(); i++) {
if(i > 0 && nn[i] == nn[i - 1]) {//避免第二个方面的重复
continue;
}
for(int j = i + 1; j < nums.size(); j++) {
//第二个数也可能相等,如输入[0,0,0,0],结果可能第一个0,第二个元素出现两个0,即为[[0,0,0],[0,0,0]]
if(j > i + 1 && nn[j] == nn[j - 1]) {
continue;
}
//从i+1开始去重第一个元素和第二个元素,避免了第一个方面的重复
//第三个元素只能在j之后找,因为避免第二个方面的重复,也自然避免了第一个方面的重复
//找第三个元素用hash表找,用set去掉了相等的情况
set<int> s(nn.begin() + j + 1, nn.end());
auto tmp = s.find(0 - nn[i] - nn[j]);
if(tmp != s.end()) {
result.push_back({nn[i], nn[j], *tmp});
}
}
}
return result;
}
};
去重思路改进为(注意重复的两个方面没变)
1、结果三元组中的三个变量分别去重(第一个元素的去重的思路一致)
2、结果三元组不能有重复
改进时间复杂度的点:
1、排序之后如果第一个元素已经大于零,那么不可能凑成三元组
2、用sort快排而不是multiset排序
3、第二,三个元素去重,但是如果仅仅判断是否与前一个相等的话又不妥,因为第一个元素和第二个元素在不是同一个元素的基础上数值是可以相等的。第三个元素只能在第二个元素之前找,一旦找到第三个元素要移走,防止后面再出现一样的第二个元素,而第二个元素自然跟着for循环往后移(第二个第三个元素去重)
确定第二,第三个元素的代码
unordered_set<int> set;
for(int j = i + 1; j < nums.size(); j++) {
//第二,三个元素去重,但是如果仅仅判断是否与前一个相等的话又不妥,因为第一个元素和第二个元素在不是同一个元素的基础上数值是可以相等的
//如输入[0,0,0,0],结果可能第一个0,第二个元素出现两个0,即为[[0,0,0],[0,0,0]]
if(j > i + 2 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2]) {
continue;
}
//从i+1开始去重第一个元素,避免了第一个方面的重复
//第三个元素只能在第二个元素之前找,一旦找到第三个元素要移走,防止后面再出现一样的第二个元素,而第二个元素自然跟着for循环往后移
//找第三个元素用hash表找,用set去掉了相等的情况(第三个元素去重)
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 第二种情况去重(避免有重复的三元组)
} else {
set.insert(nums[j]);//确保只有不同位置的元素才能加进去
}
整体注释 更清晰,更少的版本(实现一致)
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<vector<int>> res;
for (int i = 0; i < nums.size(); i++) { // 第一个元素
if (i > 0 && nums[i - 1] == nums[i]) //第一个元素第二方面去重
continue;
unordered_map<int, int> umap; // key为数值,value为下标
for (int j = i + 1; j < nums.size(); j++) { // 找第二个、第三个元素,j为第三个
if(j > i + 3 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2] && nums[j-3] == nums[j-2]) { // 第三个元素去重,找到一串相等元素的第一个 / 第二个(第二个元素可以跟第三个元素相同)
// 如输入[0,0,0,0,0],结果为[[0,0,0],[0,0,0]]
continue;
if (umap.find(- nums[i] - nums[j]) != umap.end()) {
vector<int> tmp = {nums[i], - nums[i] - nums[j], nums[j]};
res.push_back(tmp);
umap.erase(- nums[i] - nums[j]); // 第二个元素去重
}
else {
umap[nums[j]] = j;
}
}
}
return res;
}
};
修改后的代码
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> result;
//[[-1,0,1],[-1,2,-1],[0,1,-1],[2,-1,-1]],正确的[[-1,-1,2],[-1,0,1]]如果不排序没办法去重第一种情况
//暴力法
sort(nums.begin(), nums.end());
for(int i = 0; i < nums.size(); i++) {
if(nums[i] > 0) {
break;
}
if(i > 0 && nums[i] == nums[i - 1]) {//第一个元素去重
continue;
}
unordered_set<int> set;
for(int j = i + 1; j < nums.size(); j++) {
//第二,三个元素去重,但是如果仅仅判断是否与前一个相等的话又不妥,因为第一个元素和第二个元素在不是同一个元素的基础上数值是可以相等的
//如输入[0,0,0,0,0],结果可能第一个0,第二个元素出现两个0,即为[[0,0,0],[0,0,0]],但是第二个元素是可以等于第三个元素的,所以要三个元素相等再跳过去
if(j > i + 3 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2] && nums[j-3] == nums[j-2]) {
continue;
}
//从i+1开始去重第一个元素,避免了第一个方面的重复
//第三个元素只能在第二个元素之前找,一旦找到第三个元素要移走,防止后面再出现一样的第二个元素,而第二个元素自然跟着for循环往后移
//找第三个元素用hash表找,用set去掉了相等的情况(第三个元素去重)
int c = 0 - (nums[i] + nums[j]);
if (set.find(c) != set.end()) {
result.push_back({nums[i], nums[j], c});
set.erase(c);// 第二种情况去重(避免有重复的三元组)
} else {
set.insert(nums[j]);//确保只有不同位置的元素才能加进去
}
}
}
return result;
}
};
2.3 leetcode 15:三个数双指针求和
其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意
而且使用哈希法在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长
接下来介绍另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些
思路来源以及动图来自 代码随想录
拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left定义在i+1的位置上,定义下标right在数组结尾的位置上
依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]
接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些
如果 nums[i] + nums[left] + nums[right] < 0 说明 此时三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止
去重逻辑:对三个元素的降重逻辑一样,都是找到一串相等元素的第一个,用完直接跳到一串之后的那个元素,要做的是不能有重复的三元组,但三元组内的元素是可以重复的
时间复杂度:O(n2)
按思路实现代码:
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(i > 0 && nums[i - 1] == nums[i]) {// 在一串相等的里面第一个才考虑
continue;//对三个元素的降重逻辑一样,都是找到一串相等元素的第一个,用完直接跳到一串之后的那个元素,要做的是不能有重复的三元组,但三元组内的元素是可以重复的
}
int left = i + 1;
int right = nums.size() - 1;
while(left < right) {
// 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left了,从而漏掉了 0,0,0 这种三元组
if(nums[i] + nums[left] + nums[right] == 0) {
result.push_back({nums[i], nums[left], nums[right]});
left++;//产生结果之后left&right要动一下不然一旦满足条件就死循环了
right--;
}
if(nums[i] + nums[left] + nums[right] < 0) left++;
if(nums[i] + nums[left] + nums[right] > 0) right--;
//怕left一路推到right右边
while(left > i + 1 && left < nums.size() && nums[left] == nums[left - 1]) left++;//注意left++别越界,在一串相等的里面第一个才考虑,而且别忘了left < nums.size() - 1,因为left++
while(right < nums.size() - 1 && right >= 0 && nums[right] == nums[right + 1]) right--;//在一串相等的里面最后一个才考虑,而且别忘了right > i,因为right--
}
}
return result;
}
};
很多同学写本题的时候,去重的逻辑多加了对right 和left 的去重:(代码中注释部分)
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;
}
};
但细想一下,这种去重其实对提升程序运行效率是没有帮助的
拿right去重为例,即使不加这个去重逻辑,依然根据 while (right > left) 和 if (nums[i] + nums[left] + nums[right] > 0) 去完成right–的操作
多加了 while (left < right && nums[right] == nums[right + 1]) right–; 这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少判断的逻辑
最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的
所以这种去重 是可以不加的。 仅仅是 把去重的逻辑提前了而已
leetcode 1就不能使用双指针法,因为其要求返回的是索引下标,而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。如果其要求返回的是数值的话,就可以使用双指针法了
2.4 leetcode 18:四个数双指针求和
与leetcode 15一样依然是双指针求和,三个数字变成四个不过再加一层循环
第一遍代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
//因为返回元素而非位置,所以可以使用双指针
vector<vector<int>> result;
sort(nums.begin(), nums.end());// 用这种双指针之前,一定会更要先排序
for(int i = 0; i < nums.size(); i++) {
//去重,留第一个
if(i > 0 && nums[i - 1] == nums[i]) continue;
//如果第一个都大于target(且一定要大于0,不然后面负的有可能可以救回来)可以不用判断了[1000000000,1000000000,1000000000,1000000000] 0 报错,加在一起超过int的范围了
if(nums[i] >= target && nums[i] > 0) {
break;
}
for(int j = i + 1; j < nums.size(); j++) {
//去重
if(j > i + 1 && nums[j - 1] == nums[j]) continue;
int left = j + 1;
int right = nums.size() - 1;
while(left < right) {
if((long)nums[i] + nums[j] + nums[left] + nums[right] == target) {
//因为[0,0,1000000000,1000000000,1000000000,1000000000] 1000000000如果第3,4,5元素相加的话就超过int范围了,所以要加一个(long)
//long tmp = (long)nums[i] + nums[j] + nums[left] + nums[right]; // 一定要在右边加一个long
result.push_back({nums[i], nums[j], nums[left], nums[right]});
left++;
right--;
}
if((long)nums[i] + nums[j] + nums[left] + nums[right] > target) {
right--;
}
if((long)nums[i] + nums[j] + nums[left] + nums[right] < target) {
left++;
}
while(left > j + 1 && left < nums.size() && nums[left] == nums[left - 1]) left++;
//left必须是j+2开始不然left跟j相等也被排除了
while(right < nums.size() - 1 && right >= j+1 && nums[right] == nums[right + 1]) right--;
}
}
}
return result;
}
};
一样的道理,五数之和、六数之和等等都采用这种解法
对于leetcode 15双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,leetcode 18的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法
有一些细节需要注意,例如: 不要判断nums[i] > target 就返回了,三数之和可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是[-4, -3, -2, -1],target是-10,不能因为-4 > -10而跳过。但是我们依旧可以去做剪枝,逻辑变成nums[i] > target && (nums[i] >=0 || target >= 0)就可以了