1、哈希表理论基础
1.1 哈希表概述及用途
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素
牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找
一般哈希表都是用来快速判断一个元素是否出现集合里
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
1.2 哈希函数
将学生姓名映射到哈希表上就涉及到了哈希函数
通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了
如果hashCode得到的数值大于哈希表的大小了
此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模(% n)的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了
1.3 哈希碰撞
如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表同一个索引下标的位置
两个元素都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞
一般哈希碰撞有两种解决方法, 拉链法和线性探测法
1.4 常见的实现哈希表数据结构
三种数据结构
1、数组
2、set(集合)
3、map(映射)
std::unordered_set底层实现为哈希表,std::set 和std::multiset的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加
当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset
std::unordered_map底层实现为哈希表,std::map 和std::multimap的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的
在map是一个key value的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的
2、不同数据哈希表的使用
2.1 leetcode 242:map & 数组
第一遍代码,注释map的基本操作
class Solution {
public:
bool isAnagram(string s, string t) {
unordered_map<char, int> mp;//定义一个map来记录字符串s里字符出现的次数
for(char c : s) {
if(mp.find(c) == mp.end()) {
mp.insert(pair<char, int>(c, 1));//map的插入insert()
}
else {
auto temp = mp.find(c);//map寻找元素(key)find(),不用auto就是unordered_map<char, int>::iterator
temp->second++;
}
}
for(char h : t) {
if(mp.find(h) == mp.end()) {//当find找不到的时候
return false;
}
else {
auto temp = mp.find(h);
temp->second--;
}
}
for(auto iter = mp.begin(); iter != mp.end(); iter++) {//map的迭代循环
if(iter->second != 0) {
return false;
}
}
return true;
}
};
也可以直接用数组来代替unordered_map的功能,其余思想一致
定义一个数组叫做record用来记录字符串s里字符出现的次数
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25
(有一个相对较小的范围是用数组来做哈希表的基础)
再遍历字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做**+1** 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。
最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] = {0};
for (int i = 0; i < s.size(); i++) {
// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
record[s[i] - 'a']++;
}
for (int i = 0; i < t.size(); i++) {
record[t[i] - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (record[i] != 0) {
// record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。
return false;
}
}
// record数组所有元素都为零0,说明字符串s和t是字母异位词
return true;
}
};
同样在使用unordered_map的情况下,思路略有差别
最后不需要检查map是否全为0,删到0就删除节点了,下次就找不到了,因为之前已经确认长度相等,所以刚好没有节点找不到就是 字母异位词
利用下标操作map,支持使用下标添加
class Solution {
public:
bool isAnagram(string s, string t) {
unordered_map<char, int> cnt_s;
if (s.size() != t.size()) {
return false;
}
for (char c : s) {
cnt_s[c]++; // 利用下标操作map,支持使用下标添加
}
for (char c : t) {
if (cnt_s.find(c) == cnt_s.end()) {
return false;
}
else {
cnt_s[c]--; // 删到0就删除节点了,下次就找不到了,因为之前已经确认长度相等,所以刚好没有节点找不到就是 字母异位词
if (cnt_s[c] == 0)
cnt_s.erase(cnt_s.find(c));
}
}
return true;
}
};
2.2 leetcode 349:什么时候set / 什么时候数组
第一遍代码:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> s;//记录nums1的所包含的数字
unordered_set<int> re;//避免结果重复
vector<int> res;//最后返回的数组
for(int i:nums1) {
s.insert(i);
}
for(int j:nums2) {
if(s.find(j) != s.end()) {
re.insert(j);
}
}
for(int c:re) {
res.push_back(c);
}
return res;
}
};
使用数组来做哈希的题目,是因为题目都限制了数值的大小(leetcode 242: 因为是字母,26个)
直接使用set不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的
而这道题目没有限制数值的大小,就无法使用数组来做哈希表了
而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费
此时就要使用另一种结构体了set ,关于set,C++给提供了如下三种可用的数据结构:
std::set
std::multiset
std::unordered_set
使用unordered_set读写效率是最高的,并 不需要对数据进行排序,而且 还不要让数据重复
unordered_set<int> nums_set(nums1.begin(), nums1.end());//直接用数组初始化set
return vector<int>(result_set.begin(), result_set.end()); // 直接用set初始化数组
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
unordered_set<int> nums_set(nums1.begin(), nums1.end());//直接用数组初始化set
for (int num : nums2) {
// 发现nums2的元素 在nums_set里又出现过
if (nums_set.find(num) != nums_set.end()) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());//直接用set初始化数组
}
};
增添了数值范围:
1 <= nums1.length, nums2.length <= 1000
0 <= nums1[i], nums2[i] <= 1000
所以就可以使用数组来做哈希表了, 因为数组都是1000以内的
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result_set; // 存放结果,之所以用set是为了给结果集去重
int hash[1005] = {0}; // 默认数值为0
for (int num : nums1) { // nums1中出现的字母在hash数组中做记录
hash[num] = 1;
}
for (int num : nums2) { // nums2中出现话,result记录
if (hash[num] == 1) {
result_set.insert(num);
}
}
return vector<int>(result_set.begin(), result_set.end());
}
};
2.3 leetcode 202:unordered_set(无穷循环没有思路)
第一次不知道怎么判断,特别还有可能无穷循环
题目中说了会无限循环,那么也就是说求和的过程中,sum会重复出现
这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止
看了思路写代码
class Solution {
public:
bool isHappy(int n) {
int sum = 0;
unordered_set<int> s;
int tmp = n;
while(tmp != 0) {//条件为tmp%10 != 0不对,因为数字中间可能出现0
sum += (tmp % 10)*(tmp % 10); // 对数字的各个位 的分解
tmp = tmp / 10;
}
while(s.find(sum) == s.end()) {//注意find找不到的写法
if(sum == 1) {
return true;
}
s.insert(sum);
tmp = sum;
sum = 0;
while(tmp != 0) {
sum += (tmp % 10)*(tmp % 10);
tmp = tmp / 10;
}
}
return false;
}
};
对于
while(tmp != 0) {
sum += (tmp % 10)*(tmp % 10);
tmp = tmp / 10;
}
这一段可以复用,可以写成一个函数
class Solution {
public:
// 取数值各个位上的单数之和
int getSum(int n) {
int sum = 0;
while (n) {
sum += (n % 10) * (n % 10);
n /= 10;
}
return sum;
}
bool isHappy(int n) {
unordered_set<int> set;
while(1) {
int sum = getSum(n);
if (sum == 1) {
return true;
}
// 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false
if (set.find(sum) != set.end()) {
return false;
} else {
set.insert(sum);
}
n = sum;
}
}
};
类似实现
class Solution {
public:
bool isHappy(int n) {
// 反正不可能无限循环,最后应该是能绕回去的,所以要保存已经计算出来的数字
unordered_set<int> stor;
int tmp_sum = 0;
while (tmp_sum != 1) {
tmp_sum = 0; // 别忘了归0
while (n != 0) {
tmp_sum += (n % 10) * (n % 10);
n /= 10;
}
if (stor.find(tmp_sum) != stor.end())
return false;
else
stor.insert(tmp_sum);
n = tmp_sum; // 别忘了给n赋新值
}
if (tmp_sum == 1)
return true;
else
return false;
}
};
2.4 leetcode 1:unordered_map(第一遍逻辑有问题,包含multiset.insert/erase用法,迭代器加减 及 自加自减)
利用 哈希表快速查找特定数值元素的性质,遍历一个元素 快速查找另一个符合要求的元素是否在哈希表中
因为multiset会自己排序,所以没办法指定元素插入位置,如果借助数组找到下标那么由于multiset是有序的需要返回不同的下标,需要在数组找下标上动脑筋而非set上,有序插入删除位置不受控
std::multiset.insert(position, element)
它没有指定要插入的位置,它仅指向要开始搜索操作以插入的位置,以加快处理速度。插入是根据多集容器遵循的顺序完成的
vector 不可以用下标去加入别的元素
std::multiset.erase(element)
指的是借助其值从多重集中删除的特定元素。此方法将擦除此值的所有实例(所有等于这个值的全删了)
class Solution {
public:
vector<int> re_pos(int num, vector<int>& nums) { // 因为无法直接迭代器相减,所以写函数返回位置,因为可能有重复,所以返回一个vector
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == num)
res.push_back(i);
}
return res;
}
vector<int> twoSum(vector<int>& nums, int target) {
multiset<int> se(nums.begin(), nums.end());
vector<int> res;
for (auto iter = se.begin(); iter != se.end(); iter++) {
int a = *iter; // 解引用 解迭代器
se.erase(iter);
if (se.find(target - a) != se.end()) { // 快速查找元素
if (a == target - a) {
return re_pos(a, nums); // 处理有相同的数的情况,比如 target=6, [3, 3]
}
else {
vector<int> tmp = re_pos(a, nums);
res.push_back(tmp[0]);
vector<int> tmp2 = re_pos(target - a, nums);
res.push_back(tmp2[0]);
return res;
}
}
}
return res;
}
};
同样思路,但是错误使用迭代器的加减法:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
multiset<int> se(nums.begin(), nums.end());
vector<int> res;
for (auto iter = se.begin(); iter != se.end(); iter++) {
a = *iter;
se.erase(iter);
if (se.find(target - *iter) != se.end()) {
int i = iter - se.begin(); // 错误
res.push_back(i);
int j = se.find(*iter) - se.begin(); // 错误
res.push_back(j);
return res;
}
}
return res;
}
};
迭代器并不是都可以进行加减
迭代器实质上是一个指针,但是,并不是所有的容器的迭代器可以支持加减操作
能进行算术运算的迭代器只有随机访问迭代器,要求容器元素存储在连续内存空间内,即vector、string、deque的迭代器是有加减法的;
而map、set、multimap、multiset、list的迭代器是没有加减法的。他们仅支持**++itr、–itr**这些操作
it++与++it的区别
在STL中的容器使用迭代器进行遍历时,可以有it++与++it的效果是相同的,遍历的次数也是相同的,但是在STL中效率却不同:
++it(或–it)返回的是引用。
it++(或it–)返回的是临时对象。
因为iterator是类模板,使用it++这种形式要返回一个无用的临时对象,而it++是函数重载,所以编译器无法对其进行优化,所以每遍历一个元素,你就创建并销毁了一个无用的临时对象
使用哈希法:当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候
本题需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是是否出现在这个集合就应该想到使用哈希法了
2.5 使用map数据结构原因
不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适(如果用set找是否存在再去数组里找,首先对于set首先不会存相同的元素,就算用multiset,因为是有序的,所以在确保有相同元素在数组中正确找到不同下标比较麻烦)
std::map 和std::multimap 的key也是有序的
为了解决 可能出现两个整数相等的情况,所以为了避免重复,对于map元素的添加 是在找完了之后 进行
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> um;
//通过拿到一个寻找之前有没有能加起来为target,解决不可重复问题,之前的元素用map记录元素以及下标,注意key不可重复,map的查找是通过key完成的
vector<int> re;
for(int i = 0; i < nums.size(); i++) {
if(um.find(target - nums[i]) != um.end()) {
re.push_back(i);
re.push_back(um.find(target - nums[i])->second);
return re;
}
um.insert(pair<int, int>(nums[i], i));
}
return re;
}
};
另一种利用 下标添加 / 获取元素 的写法
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> umap;
vector<int> res;
for (int i = 0; i < nums.size(); i++) {
if (umap.find(target - nums[i]) != umap.end()) {
res.push_back(umap[target - nums[i]]);
res.push_back(i);
}
umap[nums[i]] = i;
}
return res;
}
};
这道题目中并不需要key有序,选择std::unordered_map效率更高
使用std::unordered_map(map/multimap通用)需要明确两点:
1、map用来做什么
2、map中key和value分别表示什么
map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)
接下来是map中key和value分别表示什么
这道题我们需要给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标
那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标
在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素