【algorithm】算法学习----哈希表

哈希表

存储结构

将一个大区间的内的数字映射到一个相对较小区间的数

可能会想到这不是离散化嘛。事实上离散化是一种特殊的哈希。离散化要求函数是单调递增的。

比如说我们将(-109–109)映射到(0-105),这样我们就需要自定义一个哈希函数h(x)完成这样的操作。

比如说h(x):x mod 105

但是我们很容易就发现对105取模这种方式有一个缺点。如果是0和105这两个数取模之后都是0。那就发生冲突了。因此对于这种冲突的解决方式为:开放寻址法拉链法

拉链法

拉链法就是开一个一维数组来模拟哈希表,每一个元素下面挂着一个链表用来存储位置冲突的元素。

概念理解题:840. 模拟散列表 - AcWing题库

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

  1. I x,插入一个数 x;
  2. Q x,询问数 x 是否在集合中出现过;

现在要进行 N 次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

接下来 N 行,每行包含一个操作指令,操作指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1≤N≤105
−109≤x≤109

输入样例:

5
I 1
I 2
I 3
Q 2
Q 5

输出样例:

Yes
No

在取模中要尽可能取质数,并且这个质数的要尽可能离2幂次远

比如说本题中给出的是105,那么我们就找一下离105最近的一个质数是谁:

#include <iostream>

using namespace std;

int main()
{
    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;
}

通过这样的代码我们可以知道离100000最近的是100003

#include <iostream>
#include <cstring>

using namespace std;

const int N = 1e5+3;

int h[N],e[N],ne[N],idx;
int n;

void insert(int x)
{
    //负数模上N会变成负数,我们将这个结果+N,然后再次模N,就可以把这个模值变成正数
    int k=(x%N+N)%N;
    //链表节点的插入
    e[idx]=x;
    ne[idx]=h[k];
    h[k]=idx++;
}

bool query(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()
{
    cin>>n;
    //起初每个哈希的槽都相当与一个头结点指向-1,我们使用-1表示null节点
    memset(h,-1,sizeof h);
    while(n--)
    {
        char op[2];
        int x;
        cin>>op;
        cin>>x;
        if(op[0]=='I')
            insert(x);
        else
        {
            if(query(x))
            puts("Yes");
            else
            puts("No");
        }
    }
}

下面讲一下什么是开放寻址法:

//开放寻址发的核心就是find()
#include <iostream>
#include <cstring>

using namespace std;

//经验:如果使用开放寻址法,则大致的范围应该扩大到数据的2~3倍
const int N = 2e5+3,null=0x3f3f3f3f;

int h[N];
int n;

int find(int x)
{
    int t=(x%N+N)%N;
    //如果当前位置不为null并且当前的位置不是x,那么就往后移;如果移到了末尾,那么就从头再开始
    while(h[t]!=null&&h[t]!=x)
    {
        t++;
        if(t==N) t=0;
    }
//这里返回值是位置;如果当前的值没有在哈希表中,那么插入的时候会因为h[t]==null从而退出
//循环,从而便于我们下一步进行插入操纵
//如果当前的值在哈希表中,那么就直接放回x的位置即可,从而方便我们下一步进行查找操作
    return t;
}

int main()
{
    cin>>n;
   //起初给每个节点都是赋值为null,这个null比10^9大得多
    memset(h,0x3f,sizeof h);
    while(n--)
    {
        char op[2];
        int x;
        cin>>op;
        cin>>x;
        int t=find(x);
        if(op[0]=='I')
        {
             h[t]=x;
        }
        else
        {
            if(h[t]!=null)
            puts("Yes");
            else
            puts("No");
        }
    }
}

字符串哈希法

字符串哈希法其实可以理解成字符串前缀哈希法。

如何求这种字符串的哈希呢?

我们可以将这个字符看成一个p进制的数字。

例如:
在这里插入图片描述
但是我们有如下的注意点:

如何求取任意字串的哈希值?
在这里插入图片描述
我们在前面的注意点就可以看到Q的取值一般为264,那么如何表示264呢?这里可以使用unsigned long long.其范围就是由264,溢出就可以直接取余。从而达到取余的目的。
这样求取字串的哈希值的时间复杂度为O(1)

因此我们对于哈希表的预处理就变得很简单,有点类似于前缀和与差分。
h[i]=h[i-1]*p+str[i]

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

#include <iostream>

using namespace std;

typedef unsigned long long ULL;

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];//这里的p[r-l+1]就是表示p^r-l+1
}

int main()
{
    cin>>n>>m>>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;
        cin>>l1>>r1>>l2>>r2;
        if(get(l1,r1)==get(l2,r2)) puts("Yes");
        else puts("No");
    }
    return 0;
}

补充

menset函数

memset()函数原型是extern void *memset(void *buffer, int c, int count)

buffer:为指针或是数组,

c:是赋给buffer的值,

count:是buffer的长度.

这个函数在socket中多用于清空数组.

如:原型是memset(buffer, 0, sizeof(buffer))

Memset 用来对一段内存空间全部设置为某个字符,一般用在对定义的字符串进行初始化值为’ '或‘/0’;

但是执行这段代码:memset(a,1,sizeof(a))
结果就会出错。发现a数组赋值全是16843009而不是我们想要的1。
为什么?
memset是按字节赋值的,取变量a的后8位二进制进行赋值。

1的二进制是(00000000 00000000 00000000 00000001),取后8位(00000001),int型占4个字节,当初始化为1时,它把一个int的每个字节都设置为1,也就是0x01010101,二进制是00000001 00000001 00000001 00000001,十进制就是16843009。

之所以输入0,-1时正确,纯属巧合。

0,二进制是(00000000 00000000 00000000 00000000),取后8位(00000000),初始化后00000000 00000000 00000000 00000000结果是0
-1,负数在计算机中以补码存储,二进制是(11111111 11111111 11111111 11111111),取后8位(11111111),则是11111111 11111111 11111111 11111111结果也是-1

而对于字符来说,memset函数是将所指向的某一块内存中的每个字节的内容全部设置为指定的ASCII值,

所以对于任何字符来说,memset都是可行的。

 memset(h,0x3f,sizeof h);
 其实就是赋值null=0x3f3f3f3f

0x3f3f3f3f

0x3f3f3f3f=1061109567

在算法竞赛中,我们常常需要用到设置一个常量用来代表“无穷大”。

比如对于int类型的数,有的人会采用INT_MAX,即0x7fffffff作为无穷大。但是以INT_MAX为无穷大常常面临一个问题,即加一个其他的数会溢出。

而这种情况在动态规划,或者其他一些递推的算法中常常出现,很有可能导致算法出问题。

所以在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。0x3f3f3f3f主要有如下好处:

0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。
0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。
可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明璐花生牛奶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值