哈希表:链地址法、开放寻址法、字符串前缀哈希法

数值哈希表:

        可以使一个庞大的数据空间映射到一个较小的空间范围,比如:将-10^9~10^9的数映射到0~10^5的数

注:哈希表可以把题目O(n)的时间复杂度降为O(1)

链地址法:

        1. 拉链法

        拉链法本质就是邻接表,将重复的元素连接在一条链上。

        槽的大小:这里的槽就是指哈希表的单元格,指的就是 N,一般而言 N取10^5或10^6后面的质数,如1e^5+3或者1e^6+3这种值。

        哈希函数:hash ( x ) = ( x % N + N ) % N 

        插入操作:利用哈希函数得到哈希值 k ,然后确定的这个槽就是 h [ k ]

例题 :

        一、题目描述

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

                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

#include<bits/stdc++.h>
#include<cstdio>
using namespace std;
const int N = 10003;
int h[N],e[N],ne[N],idx;//h数组代表槽点,e和ne是链表数组,idx表示头节点的位置
void insert(int x) {
    int k = (x % N + N ) % N;//将一个较大的数映射到一个较小的数上
    e[idx] = x;//通过链表存储同一个映射的数
    ne[idx] = h[k];
    h[k] = idx ++;//h[k]存的是链表头节点的下标
}
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);//使用%s输入可以自动去除空格和回车符
        if(*op == 'I') {//使用*op指针指向数组的第一个位置
            insert(x);
        }
        else {
            if(find(x))
                puts("Yes");
            else
                puts("No");
        }
    }
    return 0;
}

开放寻址法:

        处理冲突的思路:

                1.开一维数组,一维数组的大小是题目数据的2-3倍

                2.添加:从第k个下标开始找,直到找到第一个没有数值的位置插入数据

                3.查找:从第k个位置往后找,如果存在数值并且判断数值是否是我们要找的内容

                4.删除:不会把数值真的删除,通过创建一个数组标记这个数据是否被删除即可;如设一个bool类型的数组来标记

                大部分情况只需要使用添加和查找。

#include<bits/stdc++.h>
#include<cstdio>
using namespace std;
const int N = 200003,null = 0x3f3f3f3f;//0x3f3f3f3f趋近于无穷大
//N为找到第一个大于数据范围的最小质数
//null为映射范围外的数,代表这个位置为数值为空(即约定一个数值代表空)
int h[N];
bool find(int x) {
    int k = (x % N + N) % N;
    //将大的数值映射到小的数值上并防止出现映射到负数的情况(即将数值映射到数组下标内)
    while (h[k] != null && h[k] != x) {
        //从第k个位置开始判断,如果此位置不为空且数值不等于查询数值则看下一个位置
        k ++;
        if (k == N)//判断完最后一个位置,又从0下标开始判断
            k = 0;
        //因为数组是操作次数的两倍,所以循环一定会停止
    }
    return k;//如果存在该数值则返回该数值存储的下标,如果该数值不在哈希表中的话k就是存储该数值的位置
}
int main() {
    int n;
    scanf("%d",&n);
    memset(h,0x3f,sizeof(h));//memset是按字节初始化的,int为4个字节,所以等同于0x3f3f3f3f
    //将数组内的数值初始化为映射范围外的数值,代表这个位置为空
    while(n--) {
        char op[2];
        int x;
        scanf("%s%d",&op, &x); //使用%s输入可以自动去除空格和回车符
        int k = find(x);
        if(*op == 'I') {//使用*op指针指向数组的第一个位置
            h[k] = x;
        } else {
            if(h[k] != null)//当前数值不为约定的空值时,就代表找到了需要找的数值
                puts("Yes");
            else
                puts("No");
        }
    }
    return 0;
}

注: 

        memset函数是按照字节来初始化的

        memset(数组,0,sizeof())代表将int数组上的每一个字节都初始化为0,二进制都为0的数是0

        memset(数组,-1,sizeof())代表将int数组上的每一个字节都初始化为1,二进制都为1的数是-1

        memset(数组,0x3f,sizeof())代表将int数组上的每一个字节都初始化为0x3f,即等于0x3f3f3f3f

字符串前缀哈希法(很厉害的算法,必须掌握):

        当我们需要快速判断两个字符串是否相等时就可以用这种方法做题;KMP可以用来求循环节,这个方法不能求循环节

         字符串前缀哈希法:

                1.先预处理出来所有前缀的哈希,如h[0]代表前0个字符的哈希值;h[1]表示前1个字符的哈希值;h[2]表示前2个字符的哈希值;

                如何定义某一个前缀的哈希值:把字符串看成是P进制的数,每一位字母代表P进制数的每一位数字;如”ABCD”,且A=1,B=2……Z=26,则(ABCD)p对应的十进制数为(1*P^3+2^P^2+3*P^1+4*P^0)modQ,因为可能字符串很长,所以转换后的十进制数可能很大,所以mod上一个比较小的数,通过取mod就能把任何一个字符串映射到0~Q-1上的数了

                2.不要把某个字母映射成0

                3.假设不存在冲突的情况下使用,因为当我们的P取13113331Q取2^64次方时99.99%的情况下不会发生冲突的

                注:模2^64次幂可以使用unsigned long long 定义h数组,当数值溢出时就可以看为是取mod了(处理溢出机制是通过取模来完成的)

                4.好处:可以利用前缀哈希将所有子串的哈希都求出来

主要思想

        首先,想求l~r这段的哈希值且已知h[l-1]h[r]哈希值;因为是P进制数,所以左边为高位,右边为低位,即h[r]为P进制数的第0位,h[1]为P进制数的第r-1位 ,h[l-1]为P进制数的第0位,h[1]为P进制数的第l-2位 

        其次,我们令h[l-1]乘以P^r-l+1,使h[l-1]的P进制数向左移动若干位置,与h[r]的P进制数位置对齐

        最后,令两者的值相减,就可以得到某一段字符串长度的哈希值了

        公式:h[r]-h[l-1]*P^r-l+1

        预处理前缀哈希值:h[i]=h[i-1]*P+str[i]

        当预处理完前缀子串的数值后就可以使用O(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<cstdio>
#include<iostream>
using namespace std;
typedef unsigned long long ULL;
//将无符号长整型类型名定义成ULL,长整型的溢出机制是通过对2^64取模来进行的
const int N = 100010, P = 131;
int n,m;
char str[N];
ULL h[N],p[N];
ULL get(int l, int r) {
    return h[r] - h[l - 1] * p[r - l + 1];//使用公式求出某一段字符串的哈希值
}
int main() {
    scanf("%d%d%s", &n, &m, str+1);
    //str+1代表从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];//预处理字符串前缀哈希值
    }
    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;
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值