算法基础 - 哈希表

目录

一、存储结构

拉链法

拉链法解决冲突的方式

拉链法模板题代码实现(注释详解版)

开放寻址法

开放寻址法解决冲突方式

开放寻址法模板题代码实现(注释详解版)

二、字符串哈希(字符串前缀哈希法)

预处理

字符串哈希方式代码实现


(*注:本文中所有图片截自AcWing算法基础课yxc视频讲解)

哈希表主要作用:将一个比较复杂的比较多的数据结构映射到0~n一个相对小的范围内

哈希函数:

假设将一个−10^9 到 10^9范围内的数映射到1 到10^5范围内的一个数。

①如何写哈希函数:x mod 10^5 将一个比较大的数映射到10^5范围内的一个数。(mod模的数要取一个质数,并且离2的整次幂尽可能的远,这样冲突的概率是最小的)

②冲突:可能会把若干不同的数映射到同一个数。处理冲突:开放寻址法或拉链法。

一、存储结构

模板题AcWing 840. 模拟散列表 - AcWing

题目概述:对一个集合进行插入(I)和查询(Q)两种操作共N次,输出每次查询结果,某个数在集合中输出YES,否则输出NO。

数据范围:

1 ≤ N ≤ 10^5
−10^9 ≤ x ≤ 10^9

拉链法

拉链法解决冲突的方式

建立一个长度为 10^5 的数组,求出某个元素x的映射地址h(x),然后将x插入对应的位置,如上图将值11插到位置3。若第二个元素y的映射地址h(y) = h(x),则在已有值的下面再拉一条链存储值y,如在值11下再存储23。该条链的存储结构为之前学过的单链表。每条链的长度平均下来可以看作是一个常数,所以一般情况是哈希表的时间复杂度可以看作O(1)。在算法题中一般只有对哈希表的添加和查找两个操作。如果要实现删除操作,一般不会是真的删除,一般情况下是开一个数组,开一个bool变量,要删除时在该点标记一下。

拉链法的插入操作,插到距离槽最近的位置。

拉链法模板题代码实现(注释详解版)

#include <cstring>
#include <iostream>

using namespace std;

const int N = 100003;

int h[N];//拉链槽(一维数组)
int e[N], ne[N];//等同于链表中的e[N]和ne[N],分别表示当前值以及下一个位置指向哪里
int idx;//等同于链表中的idx,表示当前用到了哪个位置

void insert(int x)
{
    int k = (x % N + N) % N;//k为哈希值
    //由于在c++中x%N可能为负数,所以给x%N再加上N后再取模就一定是正数

    //链表的插入
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx ++ ;
}

bool find(int x)
{
    int k = (x % N + N) % N;//1.先映射
    for (int i = h[k]; i != -1; i = ne[i])//在哈希值k对应的链表里面找存不存在x
        if (e[i] == x)
            return true;

    return false;
}

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

    memset(h, -1, sizeof h);//将拉链槽清空操作,头文件为<cstring>

    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        //如果用scanf读入字符串,scanf会自动将所有空格,回车和制表符忽略掉

        if (*op == 'I') insert(x);
        else
        {
            if (find(x)) puts("Yes");
            else puts("No");
        }
    }

    return 0;
}

开放寻址法

开放寻址法解决冲突方式

只开了一个一维数组,没有用到链表。但是一维数组的长度一般要开到题目数据范围的2~3倍,这样冲突的概率才会比较低。处理冲突方式:例如求出h(x) = k,先看一下第k个位置是否为空,如果不为空就到下一个位置,直到找到一个空位,再将x放进去。查找操作,从位置h(x) = k的位置k开始往后找,先看一下是否为空,非空且为x就找到了,非空但是不是x就一直往后找,为空的话表示x不存在。

开放寻址法模板题代码实现(注释详解版)

#include <cstring>
#include <iostream>

using namespace std;

const int N = 200003;//数据范围的2倍
const int null = 0x3f3f3f3f;//约定一个标志,如果数组里面的一个数等于null,表示该位置上没有人

int h[N];

//核心操作 如果x在哈希表中存在,返回其位置;如果不存在,返回x应该存储的位置
int find(int x)
{
    int k = (x % N + N) % N;

    //遍历数组
    while (h[k] != null && h[k] != x)//如果该位置不为空且不为x则往后继续查找
    {
        k ++ ;
        if (k == N) k = 0;//t == N表示已经看找到了最后一个位置,此时要循环看第一个位置(?这里我还没懂)
    }
    return k;//如果x不在哈希表中,k就是x应该存储的位置
}

int main()
{
    memset(h, 0x3f, sizeof h);//把h里面的每一个数x初始化为0x3f,

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

    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);

        //插入操作
        if (*op == 'I') h[find(x)] = x;
 
        //查找操作
        else
        {
            if (h[find(x)] == null) puts("No");
            else puts("Yes");
        }
    }

    return 0;
}

二、字符串哈希(字符串前缀哈希法)

字符串哈希,一个比较重要的哈希方式

模板题:AcWing 841. 字符串哈希 - AcWing

题目概述:给定一个长度为n的字符串,对该字符串进行m次询问,每次询问字符串中的两个区间,判断这两个字符串子串是否完全相同。

数据范围

1 ≤ n,m ≤ 10^5

预处理

例如一个字符串str = "abcdefghijk",求哈希时,需要先预处理出所有前缀的哈希

h[0] = 0

h[1] = "a" //**h[1]等于这个字符串的哈希值,不是等于该字符串,下面同理

h[2] = "ab"(前两个字符的哈希)

h[3] = "abc"(前三个字符的哈希)

①定义某个前缀的哈希值:把字符串看成是一个p进制的数(所以字符串左边为高位,右边为低位),每个字符表示成p进制数的一个数(p一般取131或13331),然后将字符串转化为一个数字,由于该数可能比较大,所以需要对该转化后的数字模上一个比较大的数Q(Q一般取2^64),映射到从0到Q - 1的一个数。这样就能把任何一个字符串映射到从0到Q - 1的一个数了。(取模那里也可以用一个unsigned long long来存储所有的h,因为溢出等价于模上2^64)

②冲突:和前面的哈希不一样,前面的哈希容忍出现冲突并且可以处理冲突,而字符串哈希不考虑冲突,不能将字符映射成0,避免冲突,此外还要将p和Q分别取上述值,此时99%的情况下是不会出现冲突的

③用此种哈希方式配合前缀哈希的好处:可以利用前缀哈希,利用一个公式求得所有子段的哈希值

如何求出公式:

例如:已知1~(L-1)和1~R段前缀哈希值h(L-1)和h(R),要求字符串中L到R段的哈希值。

h[R] 最高位:p^(R-1) 最低位:p^0

h[L-1]最高位:p^(L-2) 最低位:p^0

将字符串转化为p进制数,左边为高位,右边为低位。1~L比1~R要短

第一步:将h(L-1)段往左移移到和h(R)对齐为止//**是哈希值的大小往左移,不是字符串往左移

所以将h(L-1)乘p^(R-L+1)

第二步:得出从L到R段的哈希值 = h(R) - h(L)*p^(R-L+1)

预处理完每一个前缀的哈希值后就可以用O(1)的时间算出任意一个子段的哈希值了

处理前缀哈希:h(i) = h(i-1)*p +str[i]

字符串哈希方式代码实现

#include <iostream>
#include <algorithm>

using namespace std;

typedef unsigned long long ULL;//C++11标准中,unsigned long long = ULL,用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()
{
    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];//str[i]不取0即可
        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");
    }

    return 0;
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值