通关数据结构 day_10 -- 哈希表

哈希表

存储结构

  1. 开放寻址法
  2. 拉链法

作用

将一个 -109~109的数 通过 x mod 105 映射到 [0,105]

mod 的数一般要取成一个质数

如果发生冲突,(将两个不一样的数,映射成了同样的数)

例子

840. 模拟散列表 - AcWing题库

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

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

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

输入格式

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

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

输出格式

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

每个结果占一行。

数据范围

1≤N≤105
−109≤x≤109

输入样例:

5
I 1
I 2
I 3
Q 2
Q 5

输出样例:

Yes
No

拉链法

开一个 一维数组 存储所有的 哈希值

比如我们第一次 将 h(11) 映射到了 3,我们就在 一维数组的 3 下面拉一条链,把 11 存下来,如果我们 又将 h(23)映射到了 3,我们就在 11 的后面存一个 23

#include<iostream>
#include<cstring>
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()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    int n;
    cin >> n;
    //清空槽
    memset(h,-1,sizeof h);
    
    while(n--)
    {
        char op[2];
        int x;
        cin >> op >> x;
        if(op[0] == 'I')
        {
            insert(x);   
        }else{
            if(find(x))
            {
                cout << "Yes" << endl;
            }else{
                cout << "No" << endl;
            }
        }
    }
    
    return 0;
}

开放寻址法

假如我们求出来 h(x) = k,如果 k 已经有人了,那么就去下一个坑位,直到我们找到一个没有数据的坑位,就把 x 放进去

那么我们在查找的时候同理,按照 哈希函数 计算出 x 的位置为 k,那么我们找到对应 k 位置的数,如果这个数 != x,那么我们就找 k+1 的数,直到找到 这个数 == x,或者是个空位就说明这个数 不存在

删除的时候,我们一般不会把 x 删掉,而是在数组中打一个标记,证明这个数不存在

#include<iostream>
#include<cstring>
using namespace std;

const int N = 200003,null = 0x3f3f3f3f;
int h[N];

int find(int x)
{
    //如果 x 存在,返回的是 x 存在的位置
    //如果 x 不存在,返回的是 x 应该存在的位置 
    int k = (x % N + N ) % N;
    while(h[k] != null && h[k] != x)
    {
        k++;
        if(k == N)
        {
            k = 0;
        }
        
    }   
    return k;
}
int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    int n;
    cin >> n;
    //清空槽
    memset(h,0x3f,sizeof h);
    
    while(n--)
    {
        char op[2];
        int x;
        cin >> op >> x;
        if(op[0] == 'I')
        {
            int k = find(x); 
            h[k] = x;
        }else{
            int k = find(x);
            if(h[k] == null)
            {
                cout << "No" << endl;
            }else{
                cout << "Yes" << endl;
            }
        }
    }
    
    return 0;
}

字符串前缀哈希法

常用于比较两个字符串是否相等

比如我们有一个字符串 str = "ABCABCDEFXCACWING"

先预处理出来所有前缀的哈希

h[0] = 0

h[1] = "A" 的哈希值

h[2] = "AB" 的哈希值

h[3] = "ABC" 的哈希值

h[4] = "ABCA" 的哈希值

...

我们将每一个字符串 看作 p进制的数,每一位上的字母,就代表着 P 进制的每一位数字

以 h[3] 为例,他一共有三个字母那就看成有 三位,第一位上的数字是 A,第二位上的数字是 B,第三位上的数字是 C

把 A 当成 1,B 当成 2,C 当成 3,那么 "ABC" 就可以看成 p进制的 123 = 1 x p2+2 x p1+ 3 x p0,这个数可能很大所以最后我们对他 mod Q

那么最后我们就可以把这个数 映射到 0~Q-1 的位置了

一般不能映射成 0 ---> 因为会把不同的字符串映射成 同一个数

一般来说 p = 131 或 13331 Q = 264 冲突会比较少

| |


​ L R

我们已知 h[L-1] 和 h[R]的哈希值,如何求 [L,R] 的哈希值

左边是高位,右边是低位

在 h[R] 里 R 就是第 0 位,1 就是 R-1位 pR-1~p0

h[L-1] 里 L-1 就是第 0 位,1 就是 L-2位 pL-2~p0

我们第一步需要将 h[L-1] 这一段往左移,移到和我们的 h[R] 对齐为止

例如 123 和 12345,123左移后 == 12300 和 12345 对齐

h[L-1] x pR-L+1

第二步就是h[R] - h[L-1] x pR-L+1

这样就可以求出来 [L,R] 这一段的哈希值了

小技巧:我们用 unsighed long long 来存所有的 h

所以:h[i] = h[i-1] x p + str[i]

模板

(1) 拉链法
    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;
    }

(2) 开放寻址法
    int h[N];

    // 如果x在哈希表中,返回x的下标;如果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;
    }


/*
字符串哈希
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
*/

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

练习

841. 字符串哈希 - AcWing题库

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

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

输入格式

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

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

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

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

输出格式

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

每个结果占一行。

数据范围

1≤n,m≤105

输入样例:

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

输出样例:

Yes
No
Yes
#include<iostream>
using namespace std;


typedef unsigned long long ULL;
const int N = 100010,P = 131;
int n,m;
char str[N];
ULL h[N],p[N];

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

int main()
{
    cin.tie(0);
    ios::sync_with_stdio(false);
    
    cin >> n >> m >> str+1;
    
    p[0] = 1;
    for(int i = 1;i <= n;i++)
    {
        p[i] = p[i-1] * P;
        h[i] = h[i-1] * P + str[i];
    }
    
    while(m--)
    {
        int l1,r1,l2,r2;
        cin >> l1 >> r1 >> l2 >> r2;
        
        if(get(l1,r1) == get(l2,r2))
        {
            cout << "Yes" << endl;
        }else{
            cout << "No" << endl;
        }
        
    }
    return 0;
}
posted @ 2022-10-17 17:55  ShibuyaKanon  阅读(49)  评论(0)    收藏  举报