hash算法理解和应用

Hash模拟散列表

这个问题要求我们把有限个数的数据范围较大的数,存储到一个较小的范围中,和离散化很像,实际上离散化就是一种特殊的hash方法,只是保证了有序,今天介绍的方法,是更常规的一种方法,或者说是一种存储结构。一般来说,hash表的存储结构分为两类,分别是开放寻址法,和拉链法,本质相差不多,但是拉链法所需空间更多,代码也更复杂,所以推荐大家使用开放寻址法来存储hash。一般来说,我们的hash函数设置起来可以用取模的方式来进行。严格来说就是对一个质数取模。找到第一个大于题目要求范围的质数来作为取模的质数,

key = (tag % constant ) 其中 constant 为质数 , 并且小于哈希表容量 。这样哈希值就分布的比较均匀,效率就不会受影响。

但是如果只是单纯的取模运算,大概率会与遇到一些冲突的情况。这里就是开放寻址法和拉链法的目的所在了,实际上,两种方法相差不多,一个是以链表的方式把冲突在每个值域下进行链状存储。一种是吧冲突平铺开,放到整个数组中,遇到冲突就向后继续找位置。接下来分别介绍一下两种方法,这里用一道模板题来帮助大家更好的理解这两种方法。

题目描述

维护一个集合,支持如下几种操作:
I x,插入一个数 x;
Q x,询问数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数 N,表示操作数量。
接下来 N 行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。
输出格式
对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No。
每个结果占一行。
数据范围
1≤N≤105
−109≤x≤109

拉链法

我们用类似链式前向星的方法来存储每个位置的链表,查询的时候只会在当前位置链表进行遍历,除非极其特殊的数据,时间复杂度可以看作O(1)。就算极端数据,也只能卡一组质数,继续换一个质数就可以解决问题了,大于100000的第一个质数是100003;

#include<bits/stdc++.h>
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()
{
    int n;
    scanf("%d", &n);
    memset(h, -1, sizeof h);
    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        if (*op == 'I') insert(x);
        else
        {
            if (find(x)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

开放寻址法

其实本质是差不多的,就是他把每个位置下的链表都改成了直接在这个数之后来进行移动,我们只需要找到每个hash后函数的值的初始位置,然后开始逐一对比,函数返回值是按顺序找到的正确存储的位置或这第一个可以存储的位置,所以一个函数可以同时实现两个功能,需要注意如果找到数组结束还没找到空,就回到队首继续找,直到找到空地址或正确的值。

#include <cstring>
#include <iostream>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;//大于200000的最小质数
int h[N];
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;
}
int main()
{
    memset(h, 0x3f, sizeof h);//按字节赋值
    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;
}

介绍完了这两种模拟散列表的hash方式,我们在来介绍一个比较常用的hash类型,字符串hash。通常用来判断同一段字符是否相同。

字符串Hash

这里我们介绍一中hash方法,字符串前缀hash法,举个简单的例子,有一个字符串ABCDE;

求hash时预处理所有前缀的hash。h[1]=‘A’的hash值。h[2]=’AB’的hash值,以此类推。对于hash值的选择,我们把字符串看作一个p进制的数。我们把A看作1,把Z看作26,注意要保证有字母映射为0,否则前缀会无法处理,把所有字母转化为数字后,我们用p进制来处理这个过程举个例子ABCD的hash值酒致(1*p^3+2 *p^2 +3 *p^1+4)mod Q这里要保证p一定是一个质数,一般我们选择P为131或者13331。Q为2^64.这种选法下hash冲突概率极低。几乎可以忽略不记,如果不放心,还可以对不同的质数多hash几次。

处理完前缀后,我们就可以通过比较hash值来判断字符串是否相同有了前缀函数之后,我们如果想算出一整个字符串中第L位到R的字符串hahs值,我们需要先把第l-1位和第R位对齐,然后用h[R]-h[L]*pR-L+1来算出这一段的hash值。对于Q264取模,我们可以直接定义数为unsigned long long来存储可以直接用自然溢出代替取模。我们只需要提前预处理好h函数的前缀和p的所有平方,就可以解决这个问题。

学习了原理之后,让我们用两道题目来熟悉一下字符串hash的用法。他在很多时候可以代替一些复杂的字符串算法。

字符串哈希
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [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

题目没有什么多余的信息,就是最基础的字符串hash模板题,我们直接套板子就行代码里的一些细节有注释。可以参考

#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int P=131,N=100010;
ULL h[N],p[N];//提前预处理出每个字符串的前缀和p进制的幂次
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];//计算一段字符hash值的公式
}
int main()
{
    int n,m;
    string s;
    scanf("%d%d", &n, &m);
    cin>>s;
    p[0] = 1;
    for (int i = 1; i <= n; i ++ )
    {
        h[i] = h[i - 1] * P + s[i-1];//处理前缀,注意字符串是从0开始,所以需要-1。
        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");//hash值相同说明字符串相同
        else puts("No");
    }
    return 0;
}

再来看一道复杂一点的题目

链接:https://ac.nowcoder.com/acm/problem/220345
题目描述 
给一个长度为n的字符串(1<=n<=200000),他只包含小写字母
找到这个字符串多少个前缀是M形字符串.
M形字符串定义如下:
他由两个相同的回文串拼接而来,第一个回文串的结尾字符和第二个字符串的开始字符可以重叠,也就是以下都是M形字符串.
abccbaabccba(由abccba+abccba组成)
abcbaabcba(有abcba+abcba组成)
abccbabccba(由abccba+abccba组成组成,但是中间的1是共用的)
a(一个单独字符也算)
输入描述:
输入一行,一个长度为n的字符串
输出描述:
输出这个字符串有多少个前缀是M形字符串
示例1
输入
abababcabcba
输出
2
说明
a是M形串
ababa是M形串
示例2
输入
abccbaabccba
输出
2
说明
a是M形串
abccbaabccba是M形串

M形字符串指的是由两个相同的回文串拼接而成,所以说M形字符串是有两个相同的回文串构成的,因此这个M形串本身就是回文串,我们只需要判断一个串是回文串的同时,他的一半也是回文串即可

至于如何判断一个串是不是回文串,这里我们使用哈希进行判断。如果一个串的正序哈希值等于其逆序哈希,则说明这个串是回文串,这道题增加了一个步骤,就是预处理出hash函数的后缀。需要注意的细节是,后缀hash的判断函数要搞清楚判断方向

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=200020,base=131;
ll p[N]{1},hl[N],hr[N];
char s[N];
int check(int l,int r){
    return hl[r]-hl[l-1]*p[r-l+1]==hr[l]-hr[r+1]*p[r-l+1];
}
int main(){
    cin>>s+1;
    int len=strlen(s+1);
    for(int i=1;i<=len;i++){
        p[i]=p[i-1]*base;
        hl[i]=hl[i-1]*base+s[i];//前缀的hash
        hr[len-i+1]=hr[len-i+2]*base+s[len-i+1];//后缀的hash
    }
    int res=0;
    for(int i=1;i<=len;i++){
        if(check(1,i)&&check(1,(i+1)>>1))如果这个串是回文串,且他的一半也是回文串,就更新答案
            res++;
    }
    cout<<res<<endl;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值