目录
前言
哈希表思想
拉链法
开放寻址法
acwing-840模拟散列表
拉链法代码如下
开放寻址法代码
前言
我对哈希表的印象就是:感觉可以类比数组,像数组的下标和该下标所对的元素之间的关系一样,就是比如ha[0]=1,那么我下标为0所对应的元素就是1,就是这样一种映射关系。只是哈希表所解决的是更复杂一些点、具有某种复杂的函数关系的映射。
另外感觉直接连想起来了关于离散化的知识,其实当时写那道题的时候就感觉也是类似哈希表的这种映射,只是当时还没学到hh。
离散化:acwing-802区间和
这里也提出了离散化是一种特殊的哈希表,它特殊的地方就在于它所处理的是数据范围较大的连续的坐标,在保持原有坐标的大小关系的基础上,将其压缩为范围小的、连续且非负的范围上,同时保持原始坐标的相对大小关系。
而哈希表是并没有什么按照坐标递增一个个映射,而是根据一定的哈希函数关系得到的散乱的映射,不具有递增的特性。
哈希表思想
一般来说我们所用到的求哈希值的办法就是直接对我们数组的最大的数据范围取模。
(有个小tip就是我们最好对大于数据范围的最大的质数进行取模,比如这道题里最大数据范围是1e5,那我知道大于1e5的最大指数是1e5+3,因此我的常数N就设为1e5+3。这样可以最大限度的减少冲突的发生。)
附上求大于数据范围的最大质数的代码
//求大于1e5的最大质数
#include<iostream>
using namespace std;
int main()
{
for(int i=100000;;i++)
{
int flag=1;
for(int j=2;j*j<=i;j++)
{
if(i%j==0){
flag=0;
break;
}
}
if(flag){
cout<<i;
break;
}
}
return 0;
}
在这种取值方式下,有可能出现不同的数取模之后有相同的哈希值,称哈希冲突。为了应对哈希冲突有两种办法:拉链法和开放寻址法
一般用哈希表处理的是插入和查询操作,不太常用删除,如果非要实现删除的话,我们一般是在数据上做标记,要删的数据更改一下标记这样的形式来实现删除,实际上是没有删除的。
拉链法
拉链法就如下图
(简直受不了太丑了🤣)
就是把同一个哈希值的数据连成一个单链表。看到这里我就想起邻接表的结构也是这样,这两个是类似的。
ha数组存储的是哈希值为k的所有数据所组成的链表的头指针。 理解了这些剩下的就是数组模拟单链表的内容了。
数组模拟单链表
这是之前写过的内容,可以去回顾一下。
关于一些比较容易迷惑的点(我之前有点迷hh),再说一下。
//头插法
e[idx]=x;
ne[idx]=ha[k];
ha[k]=idx++;
这是头插法插入数据的代码,最后的“ha[k]=idx++;” 这是记录下现在头结点的地址,这是在数组模拟单链表里的特有的部分,因为使用结构体链表的时候会自动分配空间地址,但是在数组模拟单链表的时候,只能靠下标的自增来实现地址的分配。
就是明白了这个突然感觉顿悟了数组模拟单链表。之前就是有点迷迷糊糊的感觉hh。
//单链表的遍历
for(int i=ha[k];i!=-1;i=ne[i])
{
if(e[i]==x)return 1;
}
明白了之后单链表的遍历也很容易理解这个for循环的三个条件了。
同时我们发现,其实我们表面上只建立了一个链表e和ne,但是他们其实是一个大的节点池,所谓的"多个单链表"都是存储在这个节点池里。也就是说,在e和ne数组中,是有很多个散乱分布的节点,而且他们并不是按照数组下标的顺序进行依次连接,而是有可能第一个节点的ne指针其实指向的是数组中第四个节点,而第三个节点的ne指针可能指向的是第二个节点。从而达到了好像有很多个单链表的效果,因此我们说这些链表是通过e和ne数组共享同一个节点池。这点可以明白哈。
在查找一个元素是否出现过的时候,我们只需要遍历其所属链表,直到到达链表末尾,我们规定其为-1,即ha数组所有位置的初始值,如果我们一直到了-1还没有找到这个元素,就说明数组中不存在这个元素。查找失败。这里-1可以安全地用作链表的结束标志。这里我们的判断标准是链表的末尾。
开放寻址法
开放寻址法处理冲突的方式是,如果这个数据所得的哈希值的位置已经被用过了,那就从这个位置开始往后找,直到找到空位直接把数据放进去。要实现这样的操作就要把数组开到我们规定数据范围的两到三倍,用来尽可能避免冲突,这样的好处是可以只建立一个ha数组。
使用这种方法在查找一个元素是否出现过的时候,由于我们每个元素都是直接占用了一个数组的下标位置,所以我们找的时候只能在数组中一个一个遍历,我们比较的时候是直接拿我们数组位上所存储的数据进行对比的,因此我们如何标记没有找到该元素呢?我们设一个在题目规定的数据范围之外的数,例如下面840这道题,我们规定数组初始值都为null=0x3f3f3f3f,这个数是大于10^9的,因此绝对不会与我们使用到的数据发生冲突导致歧义。这里我们判断的标准是实打实的每一个数据的真实值。只有我们向后索引不属于有效数据范围的null的时候,才能安全的判断不存在该元素。
总结一下这个问题
在使用开放寻址法时,通常需要定义一个数据范围之外的数作为标记,表示哈希表中的空位置。这个标记通常被称为“哨兵值”。这个哨兵值应该是一个我们知道不会出现在输入数据中的值。这样,当我们在哈希表中看到这个哨兵值时,就可以确定这个位置是空的,没有存储任何元素。这主要是为了处理查找操作。
acwing-840模拟散列表
有了上面的学习,理解了之后就直接看代码把,一些需要注意和解释的都写在注释里了。
拉链法代码如下
#include<iostream>
#include<cstring>//初始化数组memset的头文件
using namespace std;
const int N=1e5+3;//大于1e5的第一个质数
int ha[N],n;//ha数组存储的是:哈希值为 k 的所有数的 链表的头节点
int ne[N],e[N],idx;//e数组存储数据 ne数组存储下一个节点的地址 idx表示分配地址的下标
void Insert(int x)
{
int k=(x%N+N)%N;//计算出哈希值
//头插法
e[idx]=x;
ne[idx]=ha[k];
ha[k]=idx++;
}
bool find(int x)
{
int k=(x%N+N)%N;
for(int i=ha[k];i!=-1;i=ne[i])//单链表的遍历
{
if(e[i]==x)return 1;
}
return 0;
}
int main()
{
scanf("%d",&n);
memset(ha,-1,sizeof ha);//初始化数组值为-1
while(n--)
{
char op[2];
int x;
scanf("%s%d",op,&x);
if(op[0]=='I')Insert(x);
else{
if(find(x))puts("Yes");
else puts("No");
}
}
return 0;
}
开放寻址法代码
#include<iostream>
#include<cstring>
using namespace std;
const int N=2e5+3,null=0x3f3f3f3f;
int ha[N],n;
int find(int x)
{
int k=(x%N+N)%N;
while(ha[k]!=null && ha[k]!=x)//如果当前哈希值的位置有数据并且这个数不是我们正在处理的这个数,就要向后找能放它的位置
{
k++;
if(k==N)k=0;
}
return k;
}
int main()
{
scanf("%d",&n);
memset(ha,0x3f,sizeof ha);
while(n--)
{
char op[2];
int x;
scanf("%s%d",op,&x);
int k=find(x);
if(op[0]=='I')ha[k]=x;
else{
if(ha[k]!=null)puts("Yes");
else puts("No");
}
}
return 0;
}
memset函数简单了解一下,暂时够用就行。
两种方式都是用到了memset函数来进行数组的初始化
memset(ha,-1,sizeof ha);//初始化数组值为
他是一个一般是快速的初始化数组的一个函数,使用该函数时要引头文件 #include<cstring>
它每次以一个字节为单位进行赋值,因此才出现在开放寻址法里null应该是0x3f3f3f3f,但是在memset函数里只写了0x3f的原因。
有了上面对于这两种方法的解释,代码应该不难理解。主要理解一下在查找操作上判断标准的不同。区分清楚就可以了。
有问题欢迎指出!一起加油!!