散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存储存位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。 (来自*)

其中前边说到的离散化也是一种特殊的哈希方式,只不过离散化注重保序性,因此使用二分查找的方法。

其中存在问题就是:可能会把不同的数映射成相同的数,这就是哈希冲突,则我们处理冲突的方法就是将一组关键字映射到一个有限的连续的地址集(区间)上,到时候我们查找的时候就可以顺着这个地址依次查找。

而处理冲突的两种方法:拉链法开放寻址法

拉链法:

哈希表(hash)

这种方法就是把映射值相同的点像链表一样挂在同一个地址上,当我们寻找的时候就可以通过地址来直接索引。

而寻找这个地址或者说映射的方法就是取模(mod),而mod的数最好就是大于映射范围的第一个质数,这样会更大的减少冲突(数学推理不清楚,听大佬说的)。

取模的方法: k = (x % N + N) % N (N是映射后的范围,这样取模是为了防止负数)

开放寻址法

这种方法只需要开一个数组,不过这个数组的大小最好是映射后范围的2~3倍,那是因为这种方法再寻找映射后结果如果被占用则它顺着这个结果继续向下找直到找到空位。

说的形象一点就好比上厕所:这个坑位有人,咱就必须取下一个坑位,直到找到一个空的坑位。

哈希表(hash)

例题

模拟散列表

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

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

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

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

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

每个结果占一行。

数据范围
1 ≤ N ≤ 10^5
−10^9 ≤ x ≤ 10^9
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No

拉链法Code:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 100003; //寻找一个大于映射范围的第一个质数 最好用质数取模
int e[N], ne[N], idx, h[N];

void insert(int x)
{   
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx;
    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;
    scanf("%d", &n);

    memset(h, -1, sizeof(h));

    while(n--)
    {
        int x;
        char op[2];
        scanf("%s%d", op, &x);
        if(*op == 'I') insert(x);
        else
        {
            if(query(x)) printf("Yes\n");
            else printf("No\n");
        }
    }

    system("pause");
    return 0;
}

开放寻址法Code:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f; //数组开到个数上限的2~3倍, null表示为空
int h[N];

int finds(int x) //两个作用:1.寻找可以插入的位置 2.寻找哈希表中是否存在要查找的数字
{
    int t = (x % N + N) % N;
    while(h[t] != null && h[t] != x)
    {
        t++;
        if(t == N) t = 0; //如果找到尾则从头寻找
    }

    return t;
}

int main()
{
    memset(h, 0x3f, sizeof(h));  //寻找一个标志 这个标志大于x的范围

    int n;
    scanf("%d", &n);

    while(n--)
    {
        char op[2];
        int x;

        scanf("%s%d", op, &x);
        if(*op == 'I') h[finds(x)] = x;
        else
        {
            if(h[finds(x)] != null) puts("Yes");
            else puts("No");
        }
    }

    return 0;
}

字符串哈希

字符串哈希就是把一个字符串哈希为整数,具体方法就是把一个字符串具体看成一个P进制数(P不确定),然后我们把他换算成十进制数字,这样就可以直接通过数字来判断两个字符串是否相等。(相当厉害并且好用的一种方法)。

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

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

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

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

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

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

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

每个结果占一行。

数据范围
1 ≤ n,m ≤ 10^5
输入样例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
输出样例:
Yes
No
Yes

思路:直接把每个字符串的哈希值存入一个数组,然后对数组排序后进行判断即可。

进制P,一般取为131,1331..(同样有数学证明)。

字符串换算为10进制范围会很大,所以我们使用 unsigned long long ,溢出会自动对2^64取模

代码:

 1 #include <iostream>
 2 #include <algorithm>
 3 #include <cstring>
 4 #include <cstdio>
 5 
 6 using namespace std;
 7 
 8 typedef unsigned long long ULL;
 9 
10 const int N = 10010, P = 131;
11 char str[N];
12 ULL a[N];
13 
14 ULL hashGet(char str[])
15 {
16     int len = strlen(str);
17     ULL res = 0;
18     for(int i = 0; i < len; i++) //将P进制换算为10进制
19     {
20         res = res*P + str[i];
21     }
22 
23     return res;
24 }
25 
26 int main()
27 {
28     int n, res = 1;
29 
30     scanf("%d", &n);
31     for(int i = 0; i < n; i++)
32     {
33         scanf("%s", str);
34         a[i] = hashGet(str);
35     }
36 
37     sort(a, a+n);
38 
39     for(int i = 1; i < n; i++)
40     {
41         if(a[i] != a[i-1]) res++;
42     }
43 
44     printf("%d\n", res);
45 
46     return 0;
47 }

字符串前缀哈希法

前缀和与字符串哈希相结合,可以直接判断出一个字符串中某两段字符串是否相等,不再需要用kmp。

S = "ABCDEFG"

h[0] = 0

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

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

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

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

.......

利用前缀哈希,就可以计算出所有字符串字段的哈希值,如图求L~R的哈希值,我们已经有了hash[R]hash[L-1]的哈希值。
哈希表(hash)

同样把字符串看作P进制数,则左边就是高位 右边就是低位,要求出L~R的哈希值我们要做的就是把h[L-1]的P进制左移与R的高位对齐,相减即可。

假设进制P为2:两个字符串分别是,求hash
哈希表(hash)

现在要做的就是把2左移两位,然后相减,得出hash,所以这里的P进制数也是这个道理。

L~R计算哈希值计算公式:\(h[R] - h[L-1]*P^{L-R+1}\)

所以再判断两个字串是否相等时可以直接通过两个字符串的左右边界得出两个字符串的哈希值来判断。

例题

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

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

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

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

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

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

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

每个结果占一行。

数据范围
1 ≤ n,m ≤ 10^5
输入样例:
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 p[N], h[N];
char str[N];

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

int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    scanf("%s", str+1);

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

    while(m--)
    {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);

        if(get(l1, r1) == get(l2, r2)) puts("Yes");
        else puts("No");
    }

    system("pause");
    return 0;
}

分类:

技术点:

相关文章: