哈希表、字符串哈希初步学习

一、哈希表

哈希表h[ ]的存储结构是为了实现O(1)的复杂度查找一个数。

通过哈希函数f(x)完成映射,由关键字key得到哈希表中的下标idx:idx=f(key);

然后进行存储:h[idx]=key;

  1. 构建哈希函数: 除留取余法

    f(x) = x mod p 将x映射到 0到p-1 之间的数。

    其中:p要取成一个质数,而且要离2的整次幂尽可能远。

  2. 解决冲突问题:当f(key1)==f(key2)时

    1. 拉链法,一个h[i]处连一个单链表
    2. 开放寻址法,冲突则往后顺移,因此 h 的长度一般定义为规定范围的2到3倍

题目描述

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

“I x”,插入一个数x;
“Q x”,询问数x是否在集合中出现过;
现在要进行N次操作,对于每个询问操作输出对应的结果。

输入格式
第一行包含整数N,表示操作数量。

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

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

每个结果占一行。

数据范围
1≤N≤105
−109≤x≤109

输入样例:

5
I 1
I 2
I 3
Q 2
Q 5

输出样例:

Yes
No

1. 链地址法

和开放寻址法相比,哈希表的长度和实际要求的长度N一样即可。但是每个位置要引出一个单链表,共N条单链表,将所有f(key)为同一地址的映射在同一条单链表中。

因此,哈希表h中每个位置存储的不再是key值,而是作为一个头结点指向单链表,单链表为不包含头结点的单链表。

N个单链表的总长度也为N,理想情况下,每个单链表只有一个结点(不发生冲突)。单链表用之前学过的数组模拟,用一个数组模拟多个链表(因为有多个头结点指向)。

#include <iostream>
#include <cstring>

using namespace std;

const int N=1e5+3; "取大于1e5的第一个质数,要模质数"
int h[N]; //哈希表,作为head结点指向存储数值的e链表 ,冲突的存在同一位置
int e[N],ne[N],idx;
//这里可以从0开始存储,每个头结点都是单独拉出的head指向的
//ne[i]=-1作为链表结束的标志,表示后面没有链表了,为了避免单独考虑初始化的问题,h数组初始化为-1,初始表示空,指向结尾
//memset数组适合初始化0(00000000) -1(11111111)  0x3f3f3f3f3f  是按字节初始化
void insert(int x)
{
    
    
    "x属于-10^9到 10^9 ,模N后属于-(N-1)到N-1,要将其映射到正数的区间(0,N-1)"
    int k=(x%N+N)%N; "-(N-i)和i模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,x;
    char op[2];
    cin>>n;
    memset(h, -1, sizeof(h));  //头文件 cstring
    while (n--){
    
    
        cin>>op>>x;
        if (op[0]=='I'){
    
    
            insert(x);
        } else {
    
    
            if (find(x)) puts("Yes");
            else printf("No\n");
        }
    }
    return 0;
}

2. 开放寻址法

欲插入数值x,通过f(x)找到第一次要插入的位置,

如果位置为null,则代表数组中不存在该数,这个下标是要插入的位置;

如果不为空且该值不为x,采用线性探测序列,依次往后顺移一位,继续判断。

循环结束有两种可能:1. 碰到了null,代表了要插入的位置;2. 不为空,说明该值为x,已经查到该数。

注意:此法中哈希表定义的长度要是实际要求长度的2到3倍。

#include <iostream>
#include <cstring>

using namespace std;

const int N=2e5+3,null=0x3f3f3f3f;
int h[N];
//这次h数组里不存指向的链表了,而是就是存真实的数值,
//由于原数组值是-10^9到10^9,所以要取一个不是这个范围的数初始化h数组

int find(int x)
{
    
    
    int k=(x%N+N)%N;
    while (h[k]!=null && h[k]!=x){
    
    
        k++;
        if (k==N) k=0; //循环
    }
    //退出循环时,要么为空,要么h[k]=x,因此返回的k要么是插入的位置,要么是匹配的位置
    return k;
}

int main()
{
    
    
    int n,x;
    char op[2];
    cin>>n;
    memset(h, 0x3f, sizeof(h));  //头文件 cstring
    while (n--){
    
    
        cin>>op>>x;
        int k=find(x);
        if (op[0]=='I'){
    
    
            h[k]=x;
        } else {
    
    
            if (h[k]!=null) puts("Yes");
            else printf("No\n");
        }
    }
    return 0;
}

二、字符串哈希

详细介绍见蓝字部分。

用途:和KMP作用一样,用于字符串匹配。

思路:给定一个父串(从下标1开始存储),长度为N,将字符串视为一个P进制数,每个字符的数值可以取其ASCII码,也可以自己编码(一定要从1开始编码),然后按照前缀和的思想,将其转化为N个子串,依次求这N个子串的在P进制数下对应的数值,然后模Q,存在数组的下标1到N中。

求父串中从l到r的字符串对应的数值:h[r]-h[l-1]×pr-l+1

通常P取131 或者13331 ;Q取264

#include <iostream>

using namespace std;

typedef unsigned long long ULL;
const int N=1e5+10,P=131;
char str[N];
int l1,r1,l2,r2;
ULL h[N],p[N];//p[N]={1} 也可,除了第一位是1,其它都是0
//h[k]存储字符串前k个字母的哈希值 mod 2^64;h[0]=0,从下标1开始,前缀和思想
// p[k]存储 P^i,方便求子串,任何数的次方都为1,因此p[0]=1

ULL get_sub(int l,int r)
{
    
    
    return h[r]-h[l-1]*p[r-l+1];
}

int main()
{
    
    
    int n,m;//长度为n的字符串,再给定m个询问
    cin>>n>>m>>str+1;
    p[0]=1;
    //前缀和思想求P进制数
    for (int i = 1; i <= n; ++i) {
    
    
        p[i]=p[i-1]*P;//顺便求P^i
        h[i]=h[i-1]*P+str[i];  //由于字符串里有大写 小写 数字,直接就按照ASCII码运算,不再规定A为1了,当全是大写字母时,可以str[i]-'A'+1,即A从1开始编码,而不用ASCII码 
    }      //前缀求哈希值,每次做左移一位
    while (m--){
    
    
        cin>>l1>>r1>>l2>>r2;
        if (get_sub(l1,r1)==get_sub(l2,r2)) cout<<"Yes"<<endl;
        else cout<<"No"<<endl;
    }

    return 0;
}

利用字符串哈希可以实现O(n)复杂度实现字符串匹配,而KMP算法需要O(n2)复杂度,但是字符串哈希有出错概率,虽然极小。
KMP模板题传送

#include <iostream>

using namespace std;

typedef unsigned long long ULL;
const int N=1e5+10,M=1e6+10,R=131; 模板串p  模式串s R进制
char s[M],p[N];  p短 s长,p匹配s
ULL h[M],r[M]={
    
    1}; 即r[0]=1,其它值为0
ULL P; 大写p存模板串的哈希值

int main() {
    
    
    int n,m;
    cin>>n>>p+1>>m>>s+1;
  求模板串p的哈希值
    for (int i = 1; i <= n; ++i)  P=P*R+p[i];
  求模式串s的前缀和
    for (int i = 1; i <= m; ++i) {
    
    
        r[i]=r[i-1]*R;
        h[i]=h[i-1]*R+s[i];
    }
    for (int i = 1; i <= m-n+1; ++i) {
    
    
        if (h[i+n-1]-h[i-1]*r[n]==P) cout<<i-1<<" ";
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/113770583