哈希

提到哈希,相信很多同学都不陌生,hash(散列、杂凑)函数,是将任意长度的数据映射到有限长度的域上。在算法竞赛中被广泛应用。

那hash函数是如何实现将任意长度的数据映射到有限长度的域上的呢?通常是对一个数取模,当然有时会出现冲突,处理冲突的方法一般有两种:一种是拉链法另一种是开放寻址法,下面我将会具体对这两种实现方式作出分析

先来看下拉链法:

大概就长这个样子,如果m和n经过哈希变换后都变成了k那么他们就会变成h[k]下面链条中的一部分, h[k]储存的就是每一条链表的头节点的位置,具体实现就是用数组模拟链表来实现。

当你需要添加一个元素时,只需将经过哈希变换之后的元素添加至数组中即可,如需查找一个元素,就需要到对应哈希函数映射值下的那条链条中去找。

下面来介绍一下开放寻址法构造哈希表,哈希函数基本上是类似的,但查找与添加元素的方式就有很大的变化了,它不需要在每一个h数组下都加一条链,它只需要找到空位即可,然后按照规则一个一个找就行,如果x在哈希表中find函数返回的就是储存x的数组下标,

如果x不在哈希表中,那么返回的就是x应该存放的位置(也就是按照查找规则发现的第一个空位)

下面给出一道例题并给出两种实现方式:

 

维护一个集合,支持如下几种操作:

  1. I x,插入一个数 x;
  2. Q x,询问数 x 是否在集合中出现过;

现在要进行 NN 次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

接下来 N行,每行包含一个操作指令,操作指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 xx 在集合中出现过,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1N10^5
10^9x10^9

输入样例:

5
I 1
I 2
I 3
Q 2
Q 5

输出样例:

Yes
No

拉链法模拟散列表 
#include<iostream>
#include<cstring>
using namespace std;
const int N=100003;
int h[N],ne[N],e[N],idx;
void insert(int x)
{
    //h[k]中存的是k这个结点所生成的链表中第一个元素的下标 
    int k=(x%N+N)%N;
    e[idx]=x;
    ne[idx]=h[k];
    h[k]=idx++;
}

bool query(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;
    string c;
    int d;
    memset(h,-1,sizeof(h));
    for(int i=1;i<=n;i++)
    {
        cin>>c>>d;
        if(c=="I") insert(d);
        else 
        {
            if(query(d)) printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}


//开放寻址法模拟散列表
#include<iostream>
#include<cstring>
using namespace std;
const int N=200003,null=0x3f3f3f3f;
int h[N];

//说明:如若x存在,则返回x所在的数组下标,如若不存在,则返回应该插入的位置 
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()
{
    int n;
    cin>>n;
    string c;
    int d;
    memset(h,0x3f,sizeof(h));
    for(int i=1;i<=n;i++)
    {
        cin>>c>>d;
        int k=find(d);
        if(c=="I") h[k]=d;
        else 
        {
            if(h[k]==d)  printf("Yes\n");
            else printf("No\n");
        }
    }
    return 0;
}

 

下面再给出一个与上述两种方式不同的哈希方式:
字符串哈希:
先给出一道例题,代码中有详细讲解:


给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1][l1,r1] 和 [l2,r2][l2,r2] 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数 n 和 m,表示字符串长度和询问次数。

第二行包含一个长度为 nn 的字符串,字符串中只包含大小写英文字母和数字。

接下来 m 行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 11 开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1n,m10^5

输入样例:

8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2

输出样例:

Yes
No
Yes


#include<iostream>
#include<cstring>
using namespace std;
typedef unsigned long long ULL;
const int N=100010,P=131;
//p通常选择131或者13331;通常选择mod2^64,在ULL中爆掉对结果不产生影响 
int n,m;
char s[N];
ULL h[N],p[N];//h[i]存储前i个字符的哈希值,p[i]存储p的i次方 
ULL fun(int l,int r)
{
    return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
    cin>>n>>m>>s+1;
    p[0]=1;
    for(int i=1;i<=n;i++)
    {
        p[i]=P*p[i-1];
        h[i]=h[i-1]*P+s[i];
    }
    int l1,r1,l2,r2;
    while(m--)
    {
        cin>>l1>>r1>>l2>>r2;
        if(fun(l1,r1)==fun(l2,r2)) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
 } 

 

现在我对这道题的思路以及疑难点进行分析:
我们是将每一个字符转化为p进制下的一个数,然后对2^64进行取余;
注意:p常取131或者13331,在概率论中我们可以看到取这两个质数可以把冲突的概率降低,具体原因我在这就不分析了,这时候就要考验人品了,绝大部分题目用这两个数是没问题的,还需要注意的一点就是,2^64正好是unsigned long long的范围,所以我们可以把数据类型定义为
unsigned long long,这样即使他们在运算过程中爆掉也不会对结果产生影响。
我们处理l到r上的哈希值时,采用的是前缀和的形式,字符串左边是高位,右边是低位
下面对哈希函数做出分析:



以上就是我对哈希的全部分析啦,希望大家能够喜欢!
posted @ 2021-05-15 11:06  AC--Dream  阅读(62)  评论(0编辑  收藏  举报