哈希表的作用:简单来说就是将一个庞大的值域(复杂的数据结构)映射到一个较小的空间(例如0 ~ N,N 为1e5或1e6等比较小的数)
通常写哈希函数最简单方法就是:例如:h(x) = x mod 10^5,将很大的x映射到10^5内。但是往往会面临冲突,因为值域太大,经过哈希函数的计算映射的值相同。那么如何处理冲突呢?线性探测、二次探测、开链
这里介绍哈希表的两种存储结构:(1)开放寻址法(这里采用线性探测)(2)拉链法
概念:(1)负载系数:待插入元素的个数 / 一维数组的大小
1、拉链法 负载系数大于1
如图所示:开(例:10^5)个槽(一个一维数组),将相同映射值接在每一个槽的下面,相当于一条拉链,单链表
哈希表为一种期望算法,在平均情况下,每一条链的长度都可以看作常数,因此时间复杂度通常是很小的O(1),添加,查找,删除的话就开一个bool变量进行标记(惰性删除)。
代码例程:实现插入和查询两种操作
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003; // 取大于100000的最近的质数,离2的整次幂尽量远,冲突的概率最小
int h[N]; //槽
int e[N], ne[N]; //单链表
int idx; //当前操作数
void insert(int x)
{
int k = (x % N + N) % N; //计算哈希值的一种方式,可以保证哈希值为正
//插入单链表,从头插入
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++;
}
bool find(int x)
{
int k = (x % N + N) % N;
//遍历单链表
for(int i = h[k]; i != -1; i = ne[i])
if(e[i] == x)
return true;
return false;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h); //每个槽填充-1,相当于使单链表表头指向-1
while(n --)
{
char op[2];
int x;
scanf("%s%d", op, &x);
if(*op == 'I') insert(x);
else
{
if(find(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
SGI STL中的hash table采用的是拉链法,
2、开放寻址法
没有开拉链,形式上简单些,经验上他开的一维数组要开到两到三倍,这样冲突就会比较低了,处理冲突的方式,类似于上厕所,这个位置有人,则去下一个。
代码例程:实现插入和查找
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003; //经验值将数组开两到三倍大小,此处开两倍,最接近的质数
const int null = 0x3f3f3f3f; //设置大于1e9的数字,只要不在我们的值域范围内就可以
int h[N];
int find(int x)
{
int t = (x % N + N) % N; //哈希值
while(h[t] != null && h[t] != x) //若该位置非空,但不是x,哈希值相同的值放的位置是相邻的
{
t++; //找下一个
if(t == N) t = 0; //找到尽头,就从0开始
}
return t; //若找到,返回的就是x在的位置,若没找到,返回的就是x应该存放的位置
}
int main()
{
memset(h, 0x3f, sizeof h); //memset是按字节填充的,h为int,占4字节,则填充为0x3f3f3f3f
int n;
scanf("%d", &n);
while(n--)
{
char op[2];
int x;
scanf("%s%d", op, &x);
if(*op == 'I') h[find(x)] = x;
else
{
if(h[find(x)] == null) puts("No");
else puts("Yes");
}
}
return 0;
}
此处采用的是线性探测,也可以采用二次探测,即使用哈希函数得到元素在数组中的位置为H,但是这个位置被使用了,那么可以依次尝试H + 1^2, H + 2^2, H + 3^2, …, H + i^2。在这里,我们只要保证数组的大小为质数,并且负载系数在0.5以下(超过0.5需要重新配置并重新整理表格),那么就可以确定每插入一个元素所需要的探测次数不多于2。
3、字符串的哈希,预处理前缀的哈希
代码例程:实现判断一个长串中任意两个字串是否相同
#include <iostream>
#include <algorithm>
using namespace std;
typedef unsigned long long ULL; //溢出ULL相当于对2^64取模
const int N = 100010, P = 131; //经验值,或者取13331
int n, m;
char str[N];
ULL h[N], p[N];
ULL get(int l, int r) //获得l,r之间的哈希值
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
scanf("%d%d", &n, &m);
scanf("%s", str + 1); //从1开始
p[0] = 1;
for(int i =1; i <= n; i++)
{
h[i] = h[i - 1] * P + str[i]; //计算前缀哈希值,str[i]转换为ascii码
p[i] = p[i - 1] * P; //存储每一位上的幂次
}
while(m --)
{
int l1, r1, l2, r2;
scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
if(get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
4、STL中的set和map
set/multiset
insert() 插入一个数
find() 查找一个数
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound()/upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--