前言
哈希法牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
如果遇到需要判断一个元素是否出现过的场景,应该第一时间想到哈希法!
- 四个常见哈希表类型对比:
集合名 | 底层实现 | 是否有序 | 数值能否重复 | 能否修改数据 | 增、删、查效率 |
---|---|---|---|---|---|
set | 红黑树 | 有序 | 不重复 | 不能修改 | O(log n) |
unordered_set | 哈希表 | 无序 | 不重复 | 不能修改 | O( 1 ) |
map | 红黑树 | key有序 | key不重复 | key不能修改 | O(log n) |
unordered_map | 哈希表 | key无序 | key不重复 | key不能修改 | O( 1 ) |
应用场景:
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的;
如果需要集合是有序的,那么就用set;
如果要求不仅有序还要有重复数据的话,那么就用multiset。
-
set 和 map:
key的存储方式使用红黑树实现的,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。
虽然set、map 的底层实现是红黑树,不是哈希表,但是它们依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 -
undered_map的用法总结:
来源:【https://www.cnblogs.com/xuelisheng/p/10771961.html】:
unordered_map的迭代器是一个指针,指向这个元素,通过迭代器来取得它的值。
它的键、值分别是迭代器的first和second属性。
1 it->first; // same as (*it).first (the key value)
2 it->second; // same as (*it).second (the mapped value)
-
.find() / end()**:
返回的都是元素的迭代器(键+值)。
find()如果没找到,返回unordered_map::end。
括号内的参数是键key,但一般是“重要的信息”设成key,序号设为第二个值value。 -
map的基本操作:
来源:【https://www.cnblogs.com/xuelisheng/p/10771961.html】
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
// 构造函数
map<string, int> dict;
// 插入数据的三种方式
dict.insert(pair<string, int>("apple", 2));
dict.insert(map<string, int>::value_type("orange", 3));
dict["banana"] = 6;
// 判断是否有元素
if (dict.empty())
cout << "该字典无元素" << endl;
else
cout << "该字典共有" << dict.size() << "个元素" << endl;
// 遍历
map<string, int>::iterator iter;
for (iter = dict.begin(); iter != dict.end(); iter++)
cout << iter->first << ends << iter->second << endl;
// 查找的2种方法
if ((iter = dict.find("banana")) != dict.end()) // 返回一个迭代器指向键值为key的元素,如果没找到就返回end()
cout << "已找到banana,其value为" << iter->second << "." << endl;
else
cout << "未找到banana." << endl;
if (dict.count("watermelon") == 0) // 返回键值等于key的元素的个数
cout << "watermelon不存在" << endl;
else
cout << "watermelon存在" << endl;
pair<map<string, int>::iterator, map<string, int>::iterator> ret;
ret = dict.equal_range("banana"); // 查找键值等于 key 的元素区间为[start,end),指示范围的两个迭代器以 pair 返回
cout << ret.first->first << ends << ret.first->second << endl;
cout << ret.second->first << ends << ret.second->second << endl;
iter = dict.lower_bound("boluo"); // 返回一个迭代器,指向键值>=key的第一个元素。
cout << iter->first << endl;
iter = dict.upper_bound("boluo"); // 返回一个迭代器,指向值键值>key的第一个元素。
cout << iter->first << endl;
return 0;
}
输出:
该字典共有3个元素
apple2
banana6
orange3
已找到banana,其value为6.
watermelon不存在
banana6
orange3
orange
orange
- 【字符串】:size()和length()没有区别:
string str="0123456789";
cout <<"str.length()="<<str.length()<<endl; //结果为10
cout <<"str.size()="<<str.size()<<endl; //结果为10
//来源:https://blog.csdn.net/z_qifa/article/details/77744482
242. 有效的字母异位词
(仍然存疑,见注释)
重点:小范围的数据集,可以用数组代替哈希表,但要注意数组序号是数字,因此必须用c-‘a’(且不能是’0’)
class Solution {
public:
bool isAnagram(string s, string t) {
///unordered_map<string ,int> umap;
vector<int> hash(26,0);
//if(s.size()!=t.size()) return false;这句加上最好
for(char c: s){
hash[c-'a']++;
}
for(char c: t){
hash[c-'a']--;
//if(umap.count(c)){ umap[c]--; }
//如果硬要这样,该怎么写?(不知道,见454好像可以这样啊)(见383,貌似是错在unordered_map<string ,int> umap;应该是char,int)
//else return false;
}
for(int i=0; i<hash.size(); i++){//(char c: s){
if(hash[i]!=0)return false;//umap
}
return true;
}
};
349.两个数组的交集
-
知识点巩固:
限制了数值的大小的题目可以使用数组来做哈希。
set和multiset底层实现都是红黑树,
而unordered_set的底层实现是哈希表,读写效率是最高的,并不需要对数据进行排序,而且还不会让数据重复,但相比数组耗时、占空间大 -
区分unordered_set和unordered_map:https://blog.csdn.net/weixin_45847364/article/details/121654719
vector转成unordered_set:只能在uset构造的时候转:unordered_set <int> uset (vec.begin(), vec.end());
unordered_set转成vector:只能分两步转:(insert, 三个参数)
vector<int> vres; vres.insert(vres.end(), res.begin(), res.end());
或者强制转换vector<int> vres = vector<int>( res.begin(), res.end() );
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set <int> uset(nums1.begin(), nums1.end());
unordered_set <int> res;
for(int i: nums2){
if(uset.count(i)) res.emplace(i);
}
vector<int> vres;
vres.insert(vres.end(), res.begin(), res.end());
return vector<int>( res.begin(), res.end() );
// return vres;
}
};
202.快乐数
在这个class中写了两个函数,第一个用于得到数n每一位上的单数的平方和
必须熟练记住!如何获取一个整数的每一位:
123%10 = 3;
123/10 = 12
12%10=2;
12/10=1
1%10=1;
1/10=0
class Solution {
public:
int gosum(int n){
int sum = 0;
while(n!=0){
sum+=n%10*(n%10);
n = n/10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> uset;
while(1){
if(n==1) return true;
if(uset.count(n)) {
if(!uset.count(1)) return false;
}
uset.emplace(n);
n = gosum(n);
}
return true;
}
};
1. 两数之和
u_map 和 map 的遍历:
for中三步走
+ ->
for( unordered_map<int, int>::iterator iter = _map.begin(); iter!=_map.end(); ++iter){
int f = iter->first;
...
或者
for中:
+.
for( auto iter : _map){
int f = iter.first;
...
u_map 和 map 的插入:
_map[ni] = i;
或者:
_map.insert(pair<int, int>(nums[i], i));
以上参考【https://blog.csdn.net/sinat_18811413/article/details/120608212】
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//WAY 1
///不可以,因为要输出答案对应的下标
sort(nums.begin(), nums.end());
int left = 0, right = nums.size()-1;
while(left<right){
if(nums[left]+nums[right]>target) right--;
else if(nums[left]+nums[right]<target) left++;
else return vector<int> {left, right};
}
return vector<int> {left, right};
//WAY 2
//会错在nums = [3,3], target = 6这里:必须先判断再输入nums
unordered_map<int, int> _map;
for(int i= 0; i<nums.size(); i++) _map[nums[i]] = i;
for( unordered_map<int, int>::iterator iter = _map.begin(); iter!=_map.end(); ++iter){
int f = iter->first;
if(_map.find(target-f)!=_map.end()) return vector<int> {_map[f], _map[target-f]};
}
// for( auto iter : _map){
// int f = iter.first;
// if(_map.find(target-f)!=_map.end()) return vector<int> {_map[f], _map[target-f]};
// }
//WAY 3
for(int i= 0; i<nums.size(); i++){
int ni = nums[i];
if(_map.find(target-ni)!=_map.end()) return vector<int>{i, _map[target-ni]};
else _map[ni] = i;///或者: _map.insert(pair<int, int>(nums[i], i));
}
return {};
}
};
454.四数相加II
四个不同数组,求目标值对应的下标
class Solution {
public:
int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, vector<int>& nums4) {
unordered_map<int,int> umap;
int res = 0;
//WAY1 3重叠加,错在大数据量的用例上
for(int l = 0;l<nums4.size(); l++) umap[nums4[l]]++;
for(int i = 0; i<nums1.size(); i++){
for(int j = 0; j<nums2.size(); j++){
for(int k = 0; k<nums3.size(); k++){
int tar = 0-nums1[i]-nums2[j]-nums3[k];
if(umap.find(tar)!=umap.end()) res += umap[tar];
}
}
}
///WAY2 2重+2重,可行!
for(int a: nums1){
for(int b: nums2){
umap[a+b]++;
}
}
for(int c: nums3){
for(int d: nums4){
int tar = 0-c-d;
if(umap.count(tar)) res += umap[tar];
}
}
return res;
}
};
套路:只记录方法数量总和的题,尽量降低for嵌套层数:用其中两个+已知条件,表示剩下的变量
本题解题步骤:
首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
定义int变量count,用来统计 a+b+c+d = 0 出现的次数。
在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
最后返回统计值 count 就可以了
383.赎金信
因为题目【只有小写字母】,那可以采用一个长度为【26】的数组记录magazine里字母出现的次数。
依然是【【数组在哈希法中的应用】】。
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
///WAY1 WRONG BUT WHY ?
//unordered_map<char, int> umap;
// for(int i=0; i<ransomNote.size();i++) umap[ransomNote[i]]++;或者下一句
// for(char c: ransomNote){ umap[c]++; }
// for(char c: magazine){
// cout<<c;//a
// if(!umap.count(c)) cout<<c;cout<<9;//''9 why???
// if(umap[c]==0)return false;
// umap[c]--;
// //stdout: a9a9bb9
// }
//WAY2 RIGHT magazine和ransomNote不要搞反了
if(ransomNote.size()>magazine.size())return false;
vector<int> var(26,0);
for(char c: magazine) var[c-'a']++;
for(int i = 0; i<ransomNote.size(); i++){
int magac = ransomNote[i]-'a';
//cout<<magac<<var[magac]<<endl;
if(var[magac]==0)return false;
var[magac]--;
}
return true;
}
};
15. 三数之和(双指针题目)
- 哈希法:
不推荐 - 双指针法:
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; }
if (i > 0 && nums[i] == nums[i - 1]) { 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]});
while (right > left && nums[right] == nums[right - 1]) right--;//注意用while剪枝
while (right > left && nums[left] == nums[left + 1]) left++;
right--;//注意while更新靠【最后】的手动更新!
left++;
}
}
}
return result;
}
- 两方法共同点:1-sort(nums.begin(), nums.end()); 2- 需要去重
其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。
而且使用哈希法 在使用两层 for 循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在 leetcode 上通过,但是程序的执行时间依然比较长 。
注意:两数之和 就不能使用双指针法,因为1.两数之和要求返回的是索引下标, 而双指针法一定要排序,双指针法一定要排序,双指针法一定要排序,一旦排序之后原数组的索引就被改变了。如果要求返回的是数值的话,就可以使用双指针法了。
18. 四数之和(双指针题目)
双指针法将时间复杂度:O(n ^ k)的解法优化为 O(n ^ (k-1))的解法。也就是降一个数量级
15.三数之和的双指针解法是一层for循环 num[i] 为确定值,然后循环内有left和right下标作为双指针,找到 nums[i] + nums[left] + nums[right] = = 0。
四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] = = target的情况,三数之和的时间复杂度是O(n2),四数之和的时间复杂度是O(n3) 。
那么一样的道理,五数之和、六数之和等等都采用这种解法。
和上一题是完全一样的,建议按下面代码直接背会。
对应题型:对有重复元素值的一个数组,求其中:下标不同的、总数固定、总和固定的元素,的组合的集合。
对应难点:下标不重复的情况下对值去重,不能暴力
sort()函数默认排序为从小到大
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
if(nums.size()<4)return {};
sort(nums.begin(), nums.end());
for(int i = 0; i<nums.size(); i++){
if (nums[i] > target && nums[i] >= 0 && target >= 0) {break; }
if(i>0 && nums[i]==nums[i-1]) continue;
for(int j = i+1; j<nums.size(); j++){
if (nums[j] + nums[i] > target && nums[j] + nums[i] >= 0 && target >= 0) {break;}
if(j>i+1 && nums[j]==nums[j-1])continue;//这里要背住:一开始的去重,是“向已遍历方向”去重
int left = j+1;
int right = nums.size()-1;
while(left<right){
//long tar = nums[i]+nums[j]+nums[left]+nums[right];//
if((long)nums[i]+nums[j]+nums[left]+nums[right]<target) left++;//不懂为什么一定要这样,不能写成上一行的
else if((long)nums[i]+nums[j]+nums[left]+nums[right]>target)right--;
else{
res.push_back({nums[i], nums[j], nums[left], nums[right]});
while(left<right && nums[left]==nums[left+1]) left++;//这里要背住:1-就是找到结果以后再去重,2-而且是“向未遍历方向”去重
while(left<right && nums[right]==nums[right-1])right--;
left++;
right--;
}
}
}
}
return res;
}
};
后面没看
350.两个数组的交集||
class Solution{
public:
vector<int> intersect(vector<int>&nums1, vector<int>&nums2) {
unordered_map<int, int> m;
vector<int> intersection;
if (nums1.size() > nums2.size()){
return intersect(nums2 , nums1);
}
for (int i = 0; i <nums1.size(); i++){
m[nums1[i]]++;
}
for (int j : nums2){ //int j 记得初始化, j是nums2的每个值而不是其序号
if (m.count(j)){
//count ()函数本质上:依次检查unordered_map中是否存在具有给定键的元素,返回0/1;
//find()函数本质上:返回找到的或者end的键值对
m[j]--;
intersection.push_back(j); //vector是.push_back(), 哈希表是insert()
}
if (m[j] == 0){
m.erase(j); //哈希表.erase()
}
}
return intersection;
}
};
387.First Unique Character in a String
class Solution{
public:
int firstUniqChar(string s){
unordered_map<int, int>frequency;
for (char c : s){
frequency[c]++; //① mymap["banana"] = 22; frequency['a'] = 3
}
for(int i =0; i < s.size(); i++){ //多维数组的元素总数用sizeof()
if (frequency[s[i]] == 1){ //② 注意哈希表的写法
return i;
}
}
return -1;
}
};
第二种炫技解法,见识到了for在哈希表中的用法。。就直接CV大法好了
class Solution {
public:
int firstUniqChar(string s) {
unordered_map<int, int> position;
int n = s.size();
for (int i = 0; i < n; ++i) {
if (position.count(s[i])) {
position[s[i]] = -1;
}
else {
position[s[i]] = i;
}
}
int first = n;
for (auto [_, pos]: position) {
if (pos != -1 && pos < first) {
first = pos;
}
}
if (first == n) {
first = -1;
}
return first;
}
};
发现Leecode的题目语言和CSDN的广告语言(CH/EN)是一致的,账号一个登出了另一个也登出了!神奇子