模拟哈希表

说明

需要注意的是,这里的模板和数据结构课程中所说的有些区别,主要体现在以下几点:

  • 此模板主要是为了解决算法题,所以会采用尽可能简单的实现形式
  • 开放地址法采用的是线性探测法,但是数据结构中的线性探测是下面图片这样的,而这里的线性探测用的表长和哈希函数的mod值是一样的
    copy.png

一般哈希

链地址法

/**
 * 哈希冲突解决方法1:拉链法(数据结构中叫做链地址法),就是把哈希值相同的元素放到同一个链表中,即一个哈希值对应一个链表
 * 所以这种方法的实质就是数组模拟单链表的应用, 区别在于由一个单链表变成了多个单链表
 */
#include <iostream>
#include <cstring>

using namespace std;

const int N = 100003;

int n;
int head[N], e[N], ne[N], idx; // 每一个head都表示着每一个链表的第一个节点编号,初始值为-1代表节点为空

void insert(int x)
{
    int k = (x % N + N) % N; // + 是为了解决负数的问题
    e[idx] = x;
    ne[idx] = head[k];
    head[k] = idx ++;
}
bool find(int x)
{
    int k = (x % N + N) % N;
    for (int i = head[k]; i != -1; i = ne[i])
        if (e[i] == x) 
            return true;
    return false;
}
int main()
{
    cin >> n;
    memset(head, -1, sizeof head);
    while (n --)
    {
        int x;
        string op;
        
        cin >> op >> x;
        if (op == "Q") 
        {
            if (find(x)) cout << "Yes" << endl;
            else cout << "No" << endl;
        }
        else insert(x);
    }
    return 0;
}

开放寻址法

/**
 * 哈希冲突解决方法2:开放寻址法(下面代码用的是线性探测,但是此处是简化了的,有很多细节并没有体现出来)
 * 就是多开一些空间(一般是原数据范围的2到3倍),然后每次发生冲突时看后面有没有空间
 * 查找的时候从映射值之后开始找,要么找到那个值,那么找到空值
 * 找到那个值说明找到了,如果找到空值说明哈希表中没有这个元素,因为如果有这个元素,既然这个位置是空,那一定是可以放置在这里的,既然没放说明没有这个元素
 */
#include <iostream>
#include <cstring>

using namespace std;

const int N = 200003, null = 0x7f7f7f7f; // 理论上这里的空值只需要选择一个在-1e9~1e9之外的数即可,但是考虑到数组初始化时需要全部赋值为空,为了操作的简便,这里采用一个大于1e9的十六进制数

int n;
int head[N]; // 实际数据范围是1e5,这里的N已经是扩大2倍后的了

inline int find(int x) // 当x存在时返回它的位置,当它不存在时返回应该存放的位置
{
    int k = (x % N + N) % N;
    
    while (head[k] != null && head[k] != x) // 在没有找到要找的数据并且非空位置时就往后找
    {
        ++ k;
        if (k == N) k = 0;
    }
    // 退出循环有两种情况,要么找到空位置了,要么找到它所在的位置,返回k和上面我们说到的这个函数的功能是吻合的
    return k;
}
inline void insert(int x)
{
    head[find(x)] = x;
}
int main()
{
    cin >> n;
    memset(head, 0x7f, sizeof head);
    
    while (n --)
    {
        int x;
        string op;
        
        cin >> op >> x;
        if (op == "Q")
        {
            if (head[find(x)] != null) cout << "Yes" << endl; // 注意find返回的是位置,是否等于null看的是这个位置的数
            else cout << "No" << endl;
        }
        else insert(x);
    }
    return 0;
}

字符串哈希

说明

核心方法就是将字符串看为一个P进制数,并且结合前缀和的思路,将字符串转换为数值
有几点需要注意的是:

  • 一般哈希我们认为会产生冲突,需要解决冲突,但是在字符串哈希中我们一般认定不会产生冲突,所以代码实现上不考虑冲突解决
  • 这里说的P进制数一般选取131或者13331,将字符串转换为数值时底数为P
  • 哈希会对P进制的数转化成的数值执行取模操作,这是为了将数值压缩到一定空间内,而模数Q一般选取 \(2^64\)
  • P和Q之所以这么选择是为了尽可能减少冲突

应用示例1

copy.png

#include <iostream>

using namespace std;

typedef unsigned long long ULL;

const int N = 1e5 + 10, M = 1e6 + 10, P = 131;

int n, m;
char tem[N], s[M];
ULL h[M], p[M];

inline ULL gethash(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
    cin >> n >> (tem + 1);
    cin >> m >> (s + 1);
    
    // 获得template串hash值
    ULL hashcode = 0; // 注意所有的hashcode都应该是对Q(2^64)取模的,即都需要定义为unsigned long long溢出时才会进行取模
    for (int i = 1; i <= n; ++ i) 
        hashcode = hashcode * P + tem[i];
    
    // 获得s串字符串哈希前缀和 + p的各个幂数
    p[0] = 1;
    for (int i = 1; i <= m; ++ i)
    {
        p[i] = p[i - 1] * P;
        h[i] = h[i - 1] * P + s[i];
    }
    
    for (int i = 1; i + n - 1 <= m; ++ i)
        if (gethash(i, i + n - 1) == hashcode)
            cout << i  - 1 << ' ';
    
    return 0;
}

应用实例2

/**
 * 字符串哈希的方式:字符串前缀哈希法
 * "ABABC"
 * 它的前缀分别为
 * "A"
 * "AB"
 * "ABA"
 * "ABAB"
 * "ABABC"
 * 把每一个前缀字符串当作一个P进制数,比如把'A'映射为1,所以"ABA"=(1 * p^2 + 2 * p^1 + 1 * p^0) % Q
 * 某个字母不能映射为0,p进制下的0、000、0000转化为10进制后都是一样的,我们必须保证两个字符串的映射值一定不相同才可以区分出来
 * 之所以%Q,是因为P进制数转换为10进制数后可能很大
 * 因为是mod运算,所以难免会产生冲突,但是在字符串哈希这里,我们的P如果选择131 或者 13331,Q选择2^64,则认为在大多数情况下不会产生冲突,当然这是个玄学问题
 * 
 * 神奇且巧妙的一点是%2^64只需要将所有数据类型都定义为unsigned long long即可自动完成取模,因为无符号数溢出相当于取模,而unsigned long long最大为2^64-1,也就是2^64时为0,2^64+1为1,正好是取模的结果
 */
 
#include <iostream>

using namespace std;

typedef unsigned long long ULL;

const int N = 1e5 + 10, P = 131; // 也可以选择P = 13331

int n, m;
char str[N];
ULL h[N], p[N]; // 因为题目涉及到多次查询,每次都跑一边快速幂太耗时了,所以p数组把p的幂预处理一下

inline ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1]; // 按理说这里
}
int main()
{
    cin >> n >> m;
    cin >> 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] - 'a' + 1); // 这里的str[i] - 'a' + 1直接用str[i]也是对的,因为我们最开始就说了默认这个哈希不会产生冲突,所以只要字母的映射值不一样即可,至于是映射为1之类的还是映射为ASCII码无所谓
    }
    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 @ 2021-01-22 23:00  0x7F  阅读(116)  评论(0编辑  收藏  举报