Hash表

1、概述

主要为以下两部分:

  1. 哈希表的存储结构
  2. 字符串的哈希方式

1.1 Hash表的作用

把一堆复杂的结构映射到0~N一个小一点的范围内

如把0 ~ \(10^9\)但是其中有很多没意义的数字,把其中有意义的数字映射到一个小范围内,如0 ~ \(10^5\)

如下面模拟散列表的例子。

1.2 如何写Hash函数

x mod \(10^5\)就可以把值域比较大的数映射到\(10^5\)这个范围内了

  1. 有可能产生冲突:把两个不一样的数映射成了同一个数,如hash(1000) = 9,hash(20) = 9
  2. 处理冲突的方法:开放寻址法和拉链法

这个取模的数要取成质数,因为质数会离2的整数次幂尽可能远,数学上可以证明,这么取冲突概率最小

1.3 哈希表的存储结构

1.3.1 开放寻址法

处理冲突的思路:只开一个一维数组没有去开一个链表,这个一维数组的长度一般要开到给定范围的2~3倍。

如下题中范围为\(10^5\),那么数组要开到\(2\times 10^5\)\(3\times 10^5\)的范围,经验来讲,这个范围内冲突的概率会更小。

假设h(x) = k

添加:

那么我们先看k的位置有没有人,有人的话就去下一个位置,直到找到一个没有人的位置。插入进去。

查找:

从第k个位置开始,从前往后找,每一次先看当前位置有无人,有人且是x说明我们找到了,有人不是x就看下一个位置,当前位置没人就说明x不存在。

删除:

按照查找的方法找到x,打一个标记标明x被删除了

1.3.2 拉链法

开一个\(10^5\)的数组,把每一个位置当作一个槽,这个槽上拉出一个链,用这个链来存储当前槽上已经有的所有数

hash表作为一个期望算法,虽然每个槽都会拉一个链,但是在平均情况下,每一条链的长度可以看作常数。所以在一半情况下Hash表的时间复杂度为O(1)。

在算法题中,Hash一般都只有添加和查找两种操作,不会有删除操作。

算法题如果涉及到删除,一般不会真的删除,可以开一个bool数组,如果删除了就打一个标记

拉链寻址法的拉链实现:

2、模拟散列表题目

3、模拟散列表答案

3.1 拉链法

#include<iostream>
#include<cstring>

using namespace std;

// 找出质数
// for(int i = 100000; ; i++)
// {
//     bool flag = true;
//     for(int j = 2; j*j <= i; j++)
//         if(i%j == 0)
//         {
//             flag = false;
//             break;
//         }
//     if(flag)
//     {
//         cout << i << endl;
//         break;
//     }
// }
const int N = 100003;
int h[N]; // 存储hash表
int e[N],ne[N]; // 存储链表,e是值,ne存储下一个数组在什么位置
int 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] == k) return true;
    }
    return false;
}
int main()
{
    int n;
    cin >> n;
    memset(h,-1,sizeof h); // 空指针用-1表示
    while(n--)
    {
        char op[2];
        int x;
        cin >> op >> x;
        if(op[0] == 'I') insert(x);
        else
        {
            if(find(x)) puts("Yes");
            else puts("No");
        }
        
    }
    return 0;
}

3.2 开放寻址法

#include<iostream>
#include<cstring>

using namespace std;


const int N = 200003; // 大于20万的最小质数
const int null = 0x3f3f3f3f; // 一个大于题目中10^9的数
int h[N]; // 存储hash表

int find(int x)
{
    /**x在Hash表存在返回其位置,
     * 不存在返回其应该存储的位置
     */
    int k = (x%N+N)%N;
    while(h[k] != null && h[k] != x) // 这个位置上有数,且这个数不等于x
    {
        k++;
        if(k == N) k = 0; // 看完最后一个位置后需要循环看第一个位置
    }
    return k;
}

int main()
{
    int n;
    cin >> n;
    // memset是按字节来memset的,h是int型数组,有四个字节,每个字节都是0x3f
    memset(h,0x3f,sizeof h);
    while(n--)
    {
        char op[2];
        int x;
        cin >> op >> x;
        int k = find(x);
        if(*op == 'I') h[k] = x;
        else
        {
            if(h[k] != null) puts("Yes");
            else puts("No");
        }
        
    }
    return 0;
}

4、字符串哈希

这里涉及的是字符串前缀哈希法:

str = "ABCABCDEAPSTAR",求hash时先预处理出所有前缀的hash

hash[1]表示前1个字符的hash,h[2]表示前2个字符的hash

hash[0] = 0

hash[1] = "A"的hash值

hash[2] = "AB"的hash值

hash[3] = "ABC"的hash值

hash[4] = "ABCA"的hash值

这里有两个问题:

  1. 如何来定义某一个前缀的hash值

方法:把字符串看成1个p进制数,每一位上的字母就表示p进制数的每一位数字,指它的ASCII码

如求aStr = "ABCD"的hash值,把他看成一个四位数,首位为A,末尾为D

把A当成1,B=2,C=3,D=4,则aStr = \((1234)_p\)。转换成10进制就是\(1 \times p^3 + 2 \times p^2 + 3 \times p + 4 \times p^0\)

这个数字可能非常大,比如这个字符串有几十万个字符,我们就需要对他进行取模

\(1 \times p^3 + 2 \times p^2 + 3 \times p + 4 \times p^0\) mod N;就可以把他映射到0~N-1。

这样就可以把任意字符串映射。

一般不能映射成0,因为A = 0,那么AA也是0,那么AB和AAB就是同一个数了。

  1. 如何预处理前缀hash值

h[i] = h[i-1] \(\times\) P + str[i];

  1. 这是假设运气足够好,完全不考虑冲突的情况下

一般根据经验取:p = 131或13331,N = \(2^{64}\)

4.1 好处

可以利用前缀hash算出任意字串的hash值。

例子:已经1L-1的hash值h[L-1],1R的hash值,h[R]。如何通过这两个hash值把L~R的hash值算出来

h[R]的位数是\(p^{R-1},...,p^0\),

h[L-1]的位数是\(p^{L-2},...,p^0\),

第一步需要将2者对齐,把h[L-1]往左移使2者对齐

h[L-1] \(\times p^{R-L+1}\),

第二步:h[R]-h[L-1] \(\times p^{R-L+1}\),

取模的N是\(2^{64}\),我们只需要用unsigned long long来存储所有的h,这样就不需要取模了,它溢出就相当于取模了。

4.2 字符串哈希题目

4.3 字符串哈希答案

#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];
ULL p[N]; // 存储p的多少次方的

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

int main()
{
    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]; // str[i]不是0就可以
    }

    //询问
    while(m--)
    {
        int l1,r1,l2,r2;
        cin >> l1 >> r1 >> l2 >> r2;
        if(get(l1,r1) == get(l2,r2)) puts("Yes");
        else puts("No");
    }
    return 0;
}
posted @ 2021-03-26 10:28  晓尘  阅读(204)  评论(0)    收藏  举报