学习目的
1.学习了解哈希表基础理论和应用场景
2.掌握哈希表的常用
参考资料
红黑树
C++中set的用法
auto关键字
哈希表(Hash Table)/散列表(Key-Value)
散列算法
(一)哈希表基础知识
1.基础概念
(1)相关背景
数组特点:寻址容易、插入和删除困难;
链表特点:寻址困难、插入和删除容易;
哈希表:集查找、插入和删除一身的数据结构;
(哈希法是牺牲了空间换取了时间,用额外的数据结构存放数据实现快速查找)
应用:
用来快速判断一个元素是否出现集合里。
eg:日常学生表单中查询学生是否在名单上,就把学校里学生的名字都存在哈希表里,通过索引名字就可以查询。类比数组,将索引从下标变成其他任意你希望的样子,通过哈希函数完成这种对应关系。
(2)基础术语
Hash Table:根据Key-vaule直接进行访问的数据结构,常用的map;
Hash Function:Hash Table的映射函数,可以把任意长度的输入变成固定长度的输入,输出就是哈希值
由关键字k可以有HashFuntion很快得知存储的位置,以关键值为自变量,以实际函数计算结果为存储结构;
(3)哈希碰撞
多对一的映射
解决办法 | 详细内容 | 其他备注 |
---|---|---|
线性探测法 | 哈希表的长度比数据长度适当的大一点 | 如:同学1与同学2的名字在表中映射到了相同的地方,那就将碰撞的两个同学名字依次放入哈希表中。 |
再哈希法 | 当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时。 | 计算时间增加。 |
链地址法(拉链法) | 所有关键字为同义词的记录存储在同一线性链表中。 | 处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;但是指针需要额外的空间。 |
拉链法也可以视为一个链表数组,每一个指针指向一个链表的头结点
2.代码实现
(1)利用set读取字符串中的相关信息
unordered_set<int> nums_set(nums.begin(),nums.end());//用set记录数组中的数据信息
3.常用操作
(二)链表例题
1.有效的字母异位词
有效的字母异位词
字母的种类数目有限,所以可以考虑用数组来存储,定义一个长度为26的字符数组。
求两个字符串出现的元素个数是否相等,
一个是定义两个这样的数组再进行比较,另一个是直接在同一个数组上进行操作。
扩展到其他类型的字符统计,包括但不局限于字母。
步骤:
a.定义一个数组记录字符串
b.遍历字符串统计字符中各个字母出现的次数;
c.两个字符串一个进行正向记录,一个反向消除
d.判定数组是不是全为0元素,为0,则是异位词
重点在遍历两个数组
class Solution {
public:
bool isAnagram(string s, string t) {
int record[26] ={0};
for(int i = 0; i<s.size();i++){//分别统计两个字符串字母出现的情况
record[s[i]-'a']++;
}
for(int j = 0; j<t.size();j++){
record[t[j]-'a']--;
}
for(int k = 0;k<26;k++){
if(record[k]!=0){
return false;
}
else continue;
}
return true;
}
};
2.两个数组的交集
两个数组的交集
基本原理
与上一题的区别在于这个是具体的数值,数值元素的值分散,再用数组会造成空间的极大浪费;
std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。
unorder_set自动包含了排序和去重
步骤:
a.定义一个存放结果的unorder_set
b.将两个数组放入set中去重
c.遍历一个数组,看数组中的元素是否在另一个数组中出现过。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> result;
unordered_set<int> nums_1(nums1.begin(),nums1.end());//对nums1进行去重和排序
for(int num:nums2){ //遍历nums2中的元素
if(nums_1.find(num)!=nums_1.end()){ //如果有相同的元素就不会指向末尾了
result.insert(num);//集合用insert的方式,这就一起把nums2进行了去重
}
}
return vector<int>(result.begin(),result.end());//把unordered_set转换成vector形式
}
};
(2)哈希表达相关概念
三种访问方式:向量循秩访问Call by rank 、列表循位置访问Call by position、二叉搜索树循关键码访问Call by key、哈希循值访问Call by value。
哈希表常用的地址方法
将数据尽可能分散到哈希表的每一个项中;
定址的方法:哈希函数、处理冲突的方法
定制方法 | 定址公式 |
---|---|
直接定址法 | key=Value+C |
除法取余 | key=value%C |
数字分析法 | 分析数据的特点,根据数据的特点进行映射 |
折叠法 | 尽可能让每一位key与每一位的value都有关 |
平方取中法 | |
根据关键值直接进行访问的数据结构。 |
用哈希解决问题的三种数据结构:数组、集合、映射
3.快乐数
将数据按位数拆解求和的过程;
如果是快乐数则最终循环会变到1,不是快乐数则会无限循环。
能用哈希表解决这个问题的关键在于,不是快乐数求和的结果会重复出现
步骤:
a.写一个计算数据各个位数上数据平方和的函数
b.将每一次记录的结果存入一个set,如果数据已经出现过,return false
class Solution {
public:
int getSum(int n){
int sum=0;
while(n){
int k = n%10;
sum += k*k;
n = n/10;
}
return sum;
}
bool isHappy(int n) {
set<int> set;
while(1){//这个while(1)的用法可以由题目中提到的无限循环想到,循环体内用return打破循环
int sum = getSum(n);
//最重要的就是对sum进行判断
if(sum == 1){
return true;
}
if(set.find(sum)!=set.end()){
return false;
}else{
set.insert(sum);
}
n = sum;
}
}
};
4.两数之和
分析题目选择合适的存储方式:
数组:在记录具体数据出现次数时一般不用数组,因为哈希值太大会造成存储资源的浪费
set:只能存放一个key值既要判断符合两数之和的数是否存在,又要返回数值对应的下标位置。
map:题目中不需要key值有序,选择unordered_map更合适
步骤:
a.用map存储数组元素和下标
b.同一个元素不能重复,所以先要将目标元素移除
c.遍历数组看对应的target-nums[i]是否在容器里
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
//定义一个map存储元素值和对应下标
unordered_map<int,int> map;
//遍历整个数组找答案
for(int i=0;i<nums.size();i++){
//map.find()会返回一个迭代器,用auto比较万能
auto iter = map.find(target-nums[i]);
if(iter!=map.end()){
return{iter->second,i};
}
//将元素与已经插入到map中的元素进行搜索,对应题目要求中的同一个元素在答案里不重复出现
//map插入方式要用pair的方式
map.insert(pair<int,int>(nums[i],i));
}
return{};
}
};
5.赎金信
和之前的两个字母异位词类似的解法
之前的要求两个字符串之间每个字符出现的次数一致,
这个只需要杂志上的字母个数大于赎金信需要的个数,所以将return条件修改一下就可以。
步骤:
a.用一个数组存储magazine中出现字符的情况,出现对应字符++
b.遍历赎金信字符串,出现对应字符–
c.对整个数组中的元素进行是否小于0的判断
class Solution {
public:
bool canConstruct(string ransomNote, string magazine) {
int record[26] = {0};
for (int i = 0; i < magazine.length(); i++) {
// 通过recode数据记录 magazine里各个字符出现次数
record[magazine[i]-'a'] ++;
}
for (int j = 0; j < ransomNote.length(); j++) {
// 遍历ransomNote,在record里对应的字符个数做--操作
record[ransomNote[j]-'a']--;
// 如果小于零说明ransomNote里出现的字符,magazine没有
if(record[ransomNote[j]-'a'] < 0) {
return false;
}
}
return true;
}
};