哈希表模拟
认识(哈希表/散列表)
哈希表,这个词若是对学过Java集合的人来说,是再熟悉不过的了,一般从数据库力获取的一切属性和值的映射都可用用hash表来存储,被称为万能的HashMap.在C++的STL里有Map,Set,unorderd_map ,unorderd_set,multmap,multset.
区别:有序和无序(map和unordered_map)
-
map和unordered_map都是关联容器,用于存储键值对。但是它们的区别在于:
- map中的元素是有序的,默认按键值升序排列。unordered_map中的元素是无序的。
- map底层用红黑树实现,查找、插入、删除的时间复杂度平均为O(logN)。unordered_map底层用哈希表实现,时间复杂度平均为O(1)。
- map的迭代器是双向迭代器,unordered_map的迭代器是前向迭代器.
所以主要区别在于有序与无序,以及时间复杂度。选择使用哪个容器取决于需要。
关于排序规则:
- map中的键值排序规则取决于键的类型和比较运算符<的定义。通常会按键升序排列,但也可以自定义比较函数改变排序规则。值的排序是依赖键的排序,值不直接参与排序。
- unordered_map中元素排序规则是hash函数的输出,元素位置取决于hash值,所以可以看作无序的。
例如:
#include <map>
#include <unordered_map>
#include <string>
int main() {
// map, keyed on strings, ascending order
std::map<std::string, int> map = {{"b", 2}, {"a", 1}, {"c", 3}};
// unordered_map, keyed on strings
std::unordered_map<std::string, int> umap = {{"b", 2}, {"a", 1}, {"c", 3}};
// map iterates in order
for (auto& p : map) {
std::cout << p.first << " " << p.second << "\n";
}
//因为键是字符,按字典序输出
// a 1
// b 2
// c 3
// unordered_map iterates in arbitrary order
for (auto& p : umap) {
std::cout << p.first << " " << p.second << "\n";
}
// c 3
// a 1
// b 2
}
模拟哈希表
这只在C语言写算法时会用到,因为C++大家会普遍选择STL中的容器.
实现:
- 插入的数唯一
一般用取模的方式,把一个插入的数映射到数组中的某一位置.需要解决冲突.
两种解决映射冲突的方式 - 开放寻址法
设置一个N个大小的数组,数组通常为最大可能插入的数的2到3倍且是一个素数,若插入的数的范围是1E5,则 N = 2E5+3.这种方法不能避免冲突,只是最大可能的减少冲突.
把插入的数存放在数组里,每次映射的位置 为 ((val%N)+N)%N.若该位置的值不是设定的标记值则说明该位置已经被别的值映射过了,于是向后找,因为数组足够大,若不存在总能找到一个未被映射过的位置,要么就是返回已经存在的位置.
为什么不用 (val+N)%N?(val+N)%N 与 ((val%N)+N)%N 效果上是等价的,都可以将val映射到0到N-1的范围内。但是 ((val%N)+N)%N 的表达式更优雅一些,原因有:
- (val+N)%N 在val很大的情况下,val+N可能会溢出,导致结果失真。而((val%N)+N)%N由于先取余,减小了val,溢出的可能性更小。
#include "iostream"
#include "set"
#include "cstring"
using namespace std;
const int N =2E5+3;
int q[N];
const int null = 0x3f3f3f3f;
int find(int val)
{
int t = ((val%N) + N)%N;
while(q[t] != null && q[t] != val){
if(t++ == N) t = 0;
}
return t;
}
int main()
{
memset(q,null,sizeof(q));
char op[2];
int n = 0;
cin >> n;
while(n--)
{
scanf("%s",op);
int val;
cin>>val;
if(op[0] == 'I')
{
q[find(val)] = val;
}
else
{
int it =find(val);
if(q[it] == val)
printf("Yes\n");
else
printf("No\n");
}
}
return 0;
}
- 拉链法
把映射到相同位置的数挂载在同一个链表下.
若插入的数的个数范围为1E5,则映射到的数组的长度为N = 1E5,h[N]存储每一个映射结点所在的链表的头结点.ne[] 表示该结点的后继结点,e[]表示结点的实际值.每次插入首先找到映射的地址,然后头插法插入.查找则找到映射地址后对链表进行查找.
#include "iostream"
#include "cstring"
using namespace std;
const int N =2E5+3;
int h[N]; // 映射到的结点坐标
int idx=0; //当前可用的结点
int ne[N],e[N]; //ne结点的后继坐标 e[] 结点的实际值
void insert(int val)
{
int t =((val%N)+N)%N;
e[idx] = val;
ne[idx] = h[t];
h[t] = idx++;
}
bool find(int val)
{
int t =((val%N)+N)%N;
for(int i = h[t] ; i !=-1 ; i = ne[i])
{
if(e[i] == val) return true;
}
return false;
}
int main()
{
char op[2];
int n = 0;
cin >> n;
memset(h,-1,sizeof(h));
while(n--)
{
scanf("%s",op);
int val;
cin>>val;
if(op[0] == 'I')
{
insert(val);
}
else
{
int res = find(val);
if(res) printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
字符串hash
用法:用P进制的hash值来表示一个字符串,这种做法确实的非常的精妙.这个P的取法也有讲究,比如取值P = 131 ,1331, 133331.可以看出它是一个素数.这一点在我为单片机写传感器采样的读取代码时,经常需要避开两个传感器同时读取,因为这样就会在RTOS中存在一个不能打断的时期,因为采样对时许有严格的控制需要关闭调度器.所以我也会为他们选择一个素数的时间来采样,这样的话从数学证明的角度上说冲突的概率非常小.
一个字符’a-Z’的hash值就取它自己的ascil码值
"abc"为P进制的hash值,设a,b,c的hash值为 x,y,z.
hash(“abc”) = hash(‘a’)*P2+hash(‘b’)+P1+hash(‘c’);
= hash(“ab”)*P1+hash(‘c’)*P0;
所以推出==>
设字符串 s1,s2为两个字符串则s1s2为它们之间的拼接.
hash(s1s2) = hash(s1)*P^(s2的长度) + hash(s2)
hash(s2) = hash(s1s2) - hash(s1)*P^(s2的长度)
设t(x)表示从0到x个长度的字符串.
则hash(t(n)) = hash(t(n-1))*P + hash(最后一个字符的hash值).
如果求一个字符串的任意子串的hash值
设字符串的长度为n ,求第l到第r个长度的子串的hash值.
hash(t):前t个长度的子串的hash值
hash(l...r)(长度l到r之间的子串的hash值) = hash(r) - hash(l-1)*P^(r-l+1)
//主要是前l-1个字符
比较任意两个字符串的hash值是否相等
输入的下标从1开始.l1,r1,l2,r2.
#include "iostream"
using namespace std;
const int N = 1E5+10;
const int P = 131;
typedef unsigned long long ull;
ull p[N];
ull h[N];
int main()
{
int n,m;
cin>>n>>m;
string s;
cin>>s;
p[0] =1;
for(int i = 1 ; i <= n ; i++)
{
p[i] = p[i-1]*P; //存储P进制的值
h[i] = h[i-1]*P+s[i-1]; //从第0位字符开始计算hash值 默认ascil值位字符的hash值
//所以字符串的哈希值 h[i] : 从1 到第 i个字符的哈希值 h[i] = h[i-1] *p + s[i-1];
}
//m 个查询
while(m--)
{
int l1,l2,r1,r2;
cin>>l1>>r1>>l2>>r2;
ull res1,res2;
res1 = h[r1] -h[l1-1]*p[r1-l1+1];
res2 = h[r2] -h[l2-1]*p[r2-l2+1];
if(res1 == res2 )
{
printf("Yes\n");
}
else
printf("No\n");
}
return 0;
}
可参考一篇不错的数学证明
https://www.acwing.com/solution/content/97009/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?