哈希表(hash)

哈希

哈希表:将大而复杂的数据映射到紧凑的区间内。分为:①存储结构 

(离散化是特殊的哈希,之前讲的离散化是严格保序的 映射到区间上是连续递增的)


哈希不保序,这里讲的是一般的哈希

弊端:若数据的数值较大可以对其取模后再映射,但是取模后可能造成:
之前不相同的数值取模后映射到同一位置上了,所以引入:开放寻址法拉链法

①存储结构 

{


    1(开放寻址法


    {
    添加:
        开一个一维数组h[],长度一般要开到题目要求长度的2到3倍
        1'假设a取模后的值为k,那么就将a放到h[k]上
        2'若b取模后的值仍为k,那么就将b放到h[k+i]上(即向后遍历,直到找到一个没存放数值的h[k+i]为止)

注意:由于这里数组h[ ]的长度为题设要求的2~3倍,所以一定能找到一个位置满足上述1’,2’两种情况
        ...
    查找:
        如果当前h[k]存在并且h[k]==x那么就找到了,如果!=x那么就继续向下寻找
        如果当前h[k]不存在,那么就不需要向下寻找,即x不存在
    
    删除:
        不真正的删除,而是开一个bool类型的数组,将所要删除的数值标记一下
        后续在查询、添加操作时,对此值是否存在特判一下即可

题目:840. 模拟散列表 - AcWing题库

代码:

暴力+二分,样例11/16:
#include<bits/stdc++.h>

using namespace std;

const int N=1e5+50;
int f[N];

int main()
{
    int n,i=0;
    scanf("%d",&n);
    while(n--)
    {
        char op[5];
        int x;
        scanf("%s %d",op,&x);
        if(!strcmp(op,"I"))
        {
            f[i]=x;
            i++;
        }
        else
        {
                sort(f,f+i);
                int l=0,r=i-1;
                while(l<r)
                {
                    int mid=l + r +1 >> 1;
                    if(f[mid]>x) r=mid-1;
                    else l=mid;
                }
                if(f[l]==x) cout << "Yes" << endl;
                else cout << "No" << endl;
        }
    }
    return 0;
}

 哈希:

#include<iostream>
//memset()函数所在头文件
#include<cstring>
using namespace std;
int n;
const int N=2e5+3,null=0x3f3f3f3f;
int h[N];
            
//如果x在hash表中已经存在的话,就返回x在hash表中所在的位置
//如果不存在的话就返回x应该在hash表中存储的位置
int find(int x)
{
    int k=(x%N + N)%N;
    while(h[k]!=null && h[k]!=x)
        {
            k++;
            //如果k==N的话(最边缘),那么就让它从头开始寻找
            //由于N的长度为题解要求的2倍,所以一定能找到一个k
            //这个k要么是x在hash表中的位置,要么是它可以在hash表中存在的位置
            if(k==N) k=0;
        }
        return k;
                
}
            
int main()
{
    scanf("%d",&n);
                
    memset(h,null,sizeof h);
                
    while(n--)
        {
            char op[2];
            int x;
            scanf("%s %d",op,&x);
            int k=find(x);
            if(!strcmp(op,"I")) h[k]=x;//直接记录下x可以在hash表中插入的位置
            else
            {
            //如果x存在于hash表中,那么一定不等于null
            //如果不存在于hash表中,那么k所返回的值就是x可以在hash表中存在的位置
            //但是由于这一步是查询操作,所以h[k]==null(null是初始值)
                if(h[k]!=null) cout << "Yes" << endl;
                else cout << "No" << endl;
            }
        }
    return 0;
}

}

2(拉链法

{
    添加:
        开一个一维数组a[]存下所有数值,将数值a取模后的值(假设是3),
        就在数组a[3]的后面 “拉一条链” 将a链住
        如果又有数值b取模后仍等于3,那么就在数值a的后面再“拉一条链”将数值b链住
        (每个数组a[]下对应的链就是我们之前所学的单链表)

插入过程与前面链表的插入过程一致,可翻阅查看:.单链表.-优快云博客


        ...
    查找:
        求出数值取余后对应的是数组a[]上的那一个值,再询问此a[]上的链表是否有所要求的数值
    删除:
        不真正的删除,而是开一个bool类型的数组,将所要删除的数值标记一下
        后续在查询、添加操作时,对此值是否存在特判一下即可

代码:

#include<iostream>
//memset()函数所在头文件
#include<cstring>
using namespace std;
int n;
const int N=1e5+3;
int h[N],e[N],ne[N],idx;
            
void insert(int x)
{
    //取模,映射到数组h[]上,x可能为负数,所以先模再加 之后再次取模
    int k=(x%N+N)%N;
    e[idx]=x,ne[idx]=h[k],h[k]=idx,idx++;
}
            
bool find(int x)
{
    int k=(x%N + N)%N;
    //遍历链表h[k](链表上的第一个下标)表示数组上下标为k的下一个指向
    //每次让i指向它的下一个指向(移动指针i,使其继续向下遍历)
    //i=-1时即遍历结束(空值)
    for(int i=h[k];i!=-1;i=ne[i])
        if(e[i]==x)
            return true;
                   
    return false;
}
            
int main()
{
    scanf("%d",&n);
                
    memset(h,-1,sizeof h);
                
    while(n--)
    {
        char op[2];
        int x;
        scanf("%s %d",op,&x);
        if(!strcmp(op,"I")) insert(x);
        else
        {
            if(find(x)) cout << "Yes" << endl;
            else cout << "No" << endl;
        }
    }
return 0;
}
    

}

取模的值:

上述的N分别取的是N=2e5+3、1e5+3这里不像之前取1e5+10是因为:N是取模的值

取模的值一般满足以下规律使映射时的重复率最低:(取模的数值一般取成质数并且离2的整次幂要远的多)

计算方法:
(取模的数值一般取成质数并且离2的整次幂要远的多)
{
    //本题的操作次数不大于100000,所以从100000开始遍历(根据题设要求来初始化i)
    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;
        }
        
    }
    return 0;
    //这里输出的i就是接下来要取模的数值
}

②字符串哈希方式

字符串hash方式
    这里讲的是特殊的hash方式:字符串前缀哈希法
    类似于前缀和:
    {
    str="abcacwing",str[0]=0(特殊处理),str[1]="a",str[2]="ab",
    str[3]="abc",str[4]="abca"...
    定义某一个哈希的前缀和:把这个字符串看成是p进制的数
    每一位字母(str[i])看成是在p进制下的数字
    如果字符串str[4]="abcd",分别在p进制下,a:1,b:2,c:3,d:4

    那么str[4]在p进制下的和即为:1*p^3+2*p^2+3*p^1+4*p^0,
    这样处理就可以把字符串转化成一个数字
    但这个数字可能较大,所以mod(%)上一个数Q,那么就可以把这个大的数字映射成1~Q-1之内的数字了。

接下来通过比较数值大小即可比较两个字串是否相同。

注意:①:不能映射成0,因为0在任何进制下的值都为0,那么字符串:aasxas,axssx,cbdhd都会被映射成0
    ②:这里是假设不存在冲突,不存在重复的情况。当p=131或者13331,Q=2^64时,基本上不存在冲突
    若要模上Q(2^64)那么可以直接用unsigned long long 来存储hash值,溢出的值就是模上2^64的值。

}

这样处理的好处就是可以利用前缀哈希,来确定任意字串的哈希值

{
    这里的左边(1)是高位,右边(R)是低位(进制情况下最左边为最低位,即最左边*P^0)。所以在h[R]中R就是第0位,1就是第R-1位,h[l-1]中l-1就是第0位,1就是第l-2位。

那么如果要求L到R之间字串的hash值,那么就先要将h[l-1]的0位与h[R]的第0位对齐
这里R在l的右边,所以改变h[l-1]的0位与h[R]的第0位对齐
(不对齐0位,由于不同数位对应不同P的幂,那将导致相减毫无意义)
 比如:1*p^3+2*p^2+3*p^1+4*p^0,数位a对应的是P^3,b对应的是P^2..                                          即将h[l-1]*((P^(R-1))/(P^(l-2))

}

也就是达到数位对齐的目的字符串ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上P的二次方把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
   

最终所推得的公式:


子串的hash值为①:h[R]-h[l-1] * p[R-L+1];

所以最终任意字符串的hash值为②:h[i]=h[i-1]*P + str[i];

公式①:

公式②:

题目:841. 字符串哈希 - AcWing题库

代码:

暴力+双指针,样例:8/13:
#include<iostream>

using namespace std;

int main()
{
    int m,n;
    scanf("%d %d",&m,&n);
    char str[100050];
    scanf("%s",str);
    while(n--)
    {
        int x1,y1,x2,y2;
        scanf("%d %d %d %d",&x1,&y1,&x2,&y2);
        bool flag=true;
        while(x1<=y1 && x2<=y2)
        {
            if(str[x1-1]!=str[x2-1] ||str[y1-1] != str[y2-1])
            {
                flag=false;
                break;
            }
            else x1++,x2++,y1--,y2--;
        }
        if(flag) printf("Yes\n");
        else printf("No\n");
    }
}

 哈希字符串:

#include<bits/stdc++.h>

using namespace std;

typedef unsigned long long ULL;
const int N=1e5+10,P=131;

int n,m;
char str[N];
//p[]是对进制P的预处理,记录了P的不同幂次方
//h[]存放的是该字符串的每一个字母对应的hash值
ULL h[N],p[N];

int 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);
    
    p[0]=1;
    for(int i=1;i<=n;i++)
    {
//这里需要多次运用到P的幂次方,所以在此提前做预处理
        //预处理,这里溢出值相当于取模后的值
        p[i]=p[i-1]*P;
        //转换成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)) printf("Yes\n");
        else printf("No\n");
    }
    
    return 0;
    
}

tips:

注意上述所给出的一些小技巧,能更好的帮助理解。

而且比如:p[i]=p[i-1]*P;很好的避免了pow(P,i)的繁杂操作

倘若还是不理解就先将公式记下来

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值