哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
1、Hash主要用于信息安全领域中加密算法,它把一些不同长度的信息转化成杂乱的128位的编码,这些编码值叫做Hash值. 也可以说,Hash就是找到一种数据内容和数据存放地址之间的映射关系。
2、查找:哈希表,又称为散列,是一种更加快捷的查找技术。我们之前的查找,都是这样一种思路:集合中拿出来一个元素,看看是否与我们要找的相等,如果不等,缩小范围,继续查找。而哈希表是完全另外一种思路:当我知道key值以后,我就可以直接计算出这个元素在集合中的位置,根本不需要一次又一次的查找!
举一个例子,假如我的数组A中,第i个元素里面装的key就是i,那么数字3肯定是在第3个位置,数字10肯定是在第10个位置。哈希表就是利用利用这种基本的思想,建立一个从key到位置的函数,然后进行直接计算查找。
3、Hash表在海量数据处理中有着广泛应用。
优点:记录数据量很大的时候,处理记录的速度很快,平均操作时间是一个不太大的常数
缺点:
①它是基于数组的,数组创建后难于扩展,某些哈希表被基本填满时,性能下降得非常严重,所以程序员必须要清楚表中将要存储多少数据(或者准备好定期地把数据转移到更大的哈希表中,这是个费时的过程)。
②好的哈希函数(good hash function)的计算成本有可能会显著高于线性表或者搜索树在查找时的内部循环成本,所以当数据量非常小的时候,哈希表是低效的
③哈希表按照 key 对 value 有序枚举(ordered enumeration, 或者称有序遍历)是比较麻烦的(比如:相比于有序搜索树),需要先取出所有记录再进行额外的排序
④哈希表处理冲突的机制本身可能就是一个缺陷,攻击者可以通过精心构造数据,来实现处理冲突的最坏情况。
数组的特点是:寻址容易,插入和删除困难;
而链表的特点是:寻址困难,插入和删除容易。
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
左边很明显是个数组,数组的每个成员包括一个指针,指向一个链表的头,当然这个链表可能为空,也可能元素很多。我们根据元素的一些特征把元素分配到不同的链表中去,也是根据这些特征,找到正确的链表,再从链表中找出这个元素。
维护一个集合,支持如下几种操作:
I x
,插入一个数 x;Q x
,询问数 x 是否在集合中出现过;现在要进行 NN 次操作,对于每个询问操作输出对应的结果。
第一行包含整数 NN,表示操作数量。
接下来 NN 行,每行包含一个操作指令,操作指令为 I x
,Q x
中的一种。
对于每个询问指令 Q x
,输出一个询问结果,如果 xx 在集合中出现过,则输出 Yes
,否则输出 No
。
每个结果占一行。
1≤N≤10^5
−10^9 ≤ x ≤10^9
5
I 1
I 2
I 3
Q 2
Q 5
Yes
No
/*
* @Author: Spare Lin
* @Project: AcWing2022
* @Date: 2022/7/19 23:27
* @Description: AcWing 840. 模拟散列表 拉链法(链表)
* @URL: https://www.acwing.com/activity/content/problem/content/890/
*/
/* 拉链法 (链表)
* 拉链法: 开一个一维数组存所有的值
* 解决冲突问题:如h(11) = 3 h(22) = 3 则一直往下拉一个链表来存11 22 .....
* */
#include
#include
using namespace std;
const int N = 1e5 + 3; //取模的数尽量取成质数
//h[]散列表保存头节点的下标
//e[]保存值,ne[]保存前一个值的下标,idx表示链表的索引
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;
cin >> n;
//初始化为-1 相当于把哈希表每个位置下的head = -1 构造一个空链表
memset(h, -1, sizeof h);
while (n--) {
string op;
int x;
cin >> op >> x;
if (op == "I") insert(x);
else {
cout << (find(x) ? "Yes" : "No") << endl;
}
}
return 0;
}
插入一个关键字k,如果k被占用,则往后一个位置插入, 直到没有空间。与链接法相比,不需要指针,所以可以将指针所占用的空间存放更多的槽。
缺点在于:该方法的删除操作,如果删除了某个关键字后,无法检索到以后的关键字了。如果用一个特定的值代替,查找时间就不依赖于转载因子αα了。所以我们若要删除则定一个bool数组为那些要删除的位置打上标记即可。
/*
* @Author: Spare Lin
* @Project: AcWing2022
* @Date: 2022/7/19 23:51
* @Description: AcWing 840. 模拟散列表 开放寻址法
* @URL: https://www.acwing.com/activity/content/problem/content/890/
*/
//开放寻址法 开数组要开到题目数据的两到三倍 h(x) = k
// 操作: 添加、查找、删除(找到后打上标记 并不是真正的删除)
#include
#include
using namespace std;
//开放寻址操作过程中会出现冲突的情况,一般会开成两倍的空间,减少数据的冲突
const int N = 2e5 + 3, null = 0x3f3f3f3f; //取模的数尽量取成质数
int h[N];
int find(int x) { //返回x在哈希表中的下标,如果x不存在,返回x应该存储的位置
int k = (x % N + N) % N;
//如果位置不为空且不为x则继续向后找 如果k == N则让k = 0 再从头找
while (h[k] != null && h[k] != x) {
k ++;
if(k == N) k = 0;//如果k已经到最后,从0再开始循环查找
}
return k;
}
void insert(int x) {
int k = find(x);
h[k] = x;
}
int main() {
int n;
cin >> n;
memset(h, 0x3f, sizeof h);
while (n--) {
string op;
int x;
cin >> op >> x;
int k = find(x);
if(op == "I") {
insert(x);
}
else {
cout << (h[k] != null ? "Yes" : "No") << endl;
}
}
return 0;
}
线行探查法(Linear Probing)是开放定址法中最简单的冲突处理方法,它从发生冲突的单元起,依次判断下一个单元是否为空,当达到最后一个单元时,再从表首依次判断。直到碰到空闲的单元或者探查完全部单元为止。
对于一个散列表,在散列过程中,某些元素形成一些区块,这种现象称作一次聚集(primary clustering)。就是说,散列到区块中的任何关键字都需要多次探测才可以解决哈希碰撞,然后,把该关键字添加到相应区块的桶中。
int find(int x) {
int d = 1;//试探时偏移距离
int k = (x % N + N) % N;
while(h[k] != null && h[k] != x) { //冲突
if(k + d < N) k += d; // 向元素h[k]的右边做线性试探
else if(k - d >= 0) k -= d; // 向元素h[k]的左边做线性试探
d++;
}
return k;
}
平方探测法(Quadratic Probing)即是发生冲突时,用发生冲突的单元H(key), 加
上 1²、 2²等,即H(key) + 1²,H(key) + 2²,H(key) + 3²…直到找到空闲单元。f(i)也可以构造为:±i^2,i=1,2,3,…,k。
在实际操作中,平方探测法不能探查到全部剩余的桶。不过在实际应用中,散列表如果大小是素数,并且至少有一半是空的,那么,总能够插入一个新的关键字。若探查到一半桶仍未找一个空闲的,表明此散列表太满,应该重哈希。
平方探测法是解决线性探测中一次聚集问题的解决方法,但是,它引入了被称为二次聚集的问题——散列到同一个桶的那些元素将探测到相同的备选桶。下面的技术将会排除这个遗憾,不过要付出计算一个附加的哈希函数的代价。
int find(int x) {
int d = 1;//试探时偏移距离基数
int k = (x % N + N) % N;
while(h[k] != null && h[k] != x) { //冲突
if((k + d) << 1 < N) k += d << 1; // 向元素h[k]的右边做平方试探
else if((k - d) << 1 >= 0) k -= d << 1; // 向元素h[k]的左边做平方试探
d++;//试探失败,更新试探的距离基数
}
return k;
}