在查找相关资料时,遇到了一篇写的很好的讲解哈希表的文章,完全没有接触过哈希表的可以先看这篇:来吧!一文彻底搞定哈希表! 我来对两点稍作总结:
1 哈希表的概念
哈希表是一种高效查找的数据结构,平均时空复杂度为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;
memset(h, 0x3f, sizeof h);
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;
}
注意: ①为了方便我们把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函数详解
|