在查找相关资料时,遇到了一篇写的很好的讲解哈希表的文章,完全没有接触过哈希表的可以先看这篇:来吧!一文彻底搞定哈希表!
我来对两点稍作总结:
- 设计一个好的哈希函数(解决映射问题)
- 处理冲突的方式
1 哈希表的概念
在算法题中,有这样一类题:对较大的数据范围(eg.0 ~ 109)中较少个数的数(eg.105)进行处理,也就是“大值域,个数少”问题,我们往往会选择将 109 范围映射到 105 中(eg: x mod 105),这个映射的过程就是通过哈希函数find(x)
(也叫散列函数)来实现的。
哈希表(也叫散列表)是一种高效查找的数据结构,平均时空复杂度为O(1)
。它的实现逻辑很简单:(eg.查找一个数x
)
- 找一个数
x
- 通过哈希函数
find()
得出x
应该在哈希表h[]
的存储位置 - 若
h[find(x)] == x
说明找到了,find(x)
为存储位置
若h[find(x)] != x
说明未找到,find(x)
为x
在h[]
中应该存储的位置
在查找过程中不涉及遍历操作,无论查找多少次,都可以在一次计算后找到目标,耗时/耗空都为常量,所以时空复杂度为O(1)
。(平均来看)
在不考虑冲突的情况下,find(x)
函数是一次计算,如果有冲突的话,最坏的情况需要遍历,这时时间复杂度就是O(n)
了,所以,哈希查找高性能的关键就是怎样设计一个好的哈希函数find()
来减少冲突。同时,我们也应掌握处理冲突的方式。
2 哈希函数的设计
哈希函数的设计有很多方法:直接定址法、数字分析法、平方取中法、折叠法、除留余数法、随机数法。在算法题中,我们常常选用除留余数法。
假定散列表长为N
,我们可以任取P(P <= N)
,除留余数的一般实现形式为:x mod P
。
这里需要注意两点:
①由于余数的约定为正数,但在C++中,负数的余数是负数,所以我们的映射关系可以设计为:(x mod P + P) mod P
②根据经验所谈,P
的选择一般是接近数据规模N
的质数。因为这里求质数没有时间要求,所以我们可以简单的这样计算出> N
的最近的质数:
const int N = ...;
for(int i = N; ;i ++){
bool flag = true;
for(int j = 2; j * j <= i; j ++){
if(i % j == 0){
flag = false;
break;
}
}
if(flag){
cout << i << endl;
break;
}
}
3 处理冲突的方式
哈希函数可能会出现冲突,即find(x1) = find(x2)
,对于这样的情况我们给出两种解决方式:
- 开放寻址法
- 拉链法
开放寻址法和拉链法都很常用,看习惯了,我个人感觉开放寻址法简单些,用着顺手。
3.1 开放寻址法
插入x时,如果出现冲突,则往后一个位置插入,直到没有空间。为了保证哈希表h[]
有足够多的空间,在初始化时h[]
往往需要开到数据量N
的2~3倍。
在算法实现时,不论是插入还是查找都可以用一个find()
函数来实现:
const int P = 200003, null = 0x3f3f3f3f;
// main
memset(h, 0x3f, sizeof h);
// find()
int find(int x){
int t = (x % N + N) % N;
while(h[t] != null && h[t] != x){
t ++;
if(t == N) t = 0; // 遍历到末尾的话再转回来
}
return t; // 若在哈希表中,t为下标(h[t] == x);若不在哈希表中,t为应该存储的位置(h[t] == null)
}
注意:
①为了方便我们把N
和P
统一了,就把靠近N
的那个质数P
当作N
就可以。
②可以观察到在find()
前面我多加了h[]
的初始化过程:哈希表的每个槽位初始值应该为:null。题目的数据规模是10-9~109,所以null可设为0x3f3f3f3f
,这个值比109多一点,可以作为null。
同时要注意一点就是memset()
函数是按字节赋值,因为int是4个字节,所以每个字节赋值为0x3f
就可以将每个槽位初始化为0x3f3f3f3f
。
大部分情况下memset()
的赋值都会选用0
或-1
,更多关于memset()
的使用可参考:memset函数及其用法,C语言memset函数详解。
3.2 拉链法
拉链法就是将每个槽位看成单链表,初始值为空,每进来一个就插入一个结点即可。
拉链法在初始化时h[]
开到N
即可,相比开放寻址法,它付出的额外空间复杂度在链表上了。
同时,它需要insert()
和find()
两个函数来实现:
const int N = 100003;
int h[N], e[N], ne[N], 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;
}
如果不熟悉单链表的操作可以参考:【数据结构】单链表、双链表的算法实现
4 总代码
哈希这里往往只用实现插入和查找两个操作,删除操作很少涉及。
题目链接:模拟散列表
4.1 除留余数法 + 开放寻址法
#include <cstring>
#include <iostream>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
int main()
{
memset(h, 0x3f, sizeof h);
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;
}
4.2 除留余数法 + 拉链法
#include <cstring>
#include <iostream>
using namespace std;
const int N = 100003;
int h[N], e[N], ne[N], 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);
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;
}
5 参考
[1] AcWing(dbq之前一直没给这个链接)
[2] 【数据结构】单链表、双链表的算法实现
[3] 来吧!一文彻底搞定哈希表!
[4] memset函数及其用法,C语言memset函数详解