数据结构学习-哈希表

本文介绍了哈希表的概念及其解决数组越界问题的作用,详细讲解了开放寻址法和拉链法两种处理冲突的方法,并通过实例展示了如何实现。此外,还探讨了字符串哈希,特别是字符串前缀哈希法,用于降低冲突的可能性。文章最后预告了树哈希的内容,但未展开讨论。
摘要由CSDN通过智能技术生成

哈希表(散列表)

一、引言

先来看一道题目:

给定一个数组a含有n个正整数,给定一个数组b含有m个正整数,问b数组中有多少个数在a中出现过?
有一种最朴素的想法是:直接暴力枚举:

for(int i = 0;i<a.size();i++)
{
    for(int j = 0;j<b.size();j++)
    {
        if(a[i]==b[j])
        {    cnt++;break;}
    }
}//容易知道,这个代码的时间复杂度是O(n^2)的

有没有更快的算法呢?
考虑空间换时间,不放开个数组记录b数组的数字是否出现,然后再去a数组中遍历判断即可,时间复杂度O(n)

bool st[N];
for(int i = 0;i<b.size();i++)
{
    st[b[i]] = true;
}
for(int i = 0;i<a.size();i++)
{
    if(st[a[i]]) cnt++;
}

容易发现,当数字比较大,超过1e8的时候,数组就会越界了,因为开辟不了这么大的空间,甚至下标不是数字,而是字符串,这种情况是无法先标记再遍历的,那么要引入哈希表的概念。

二、概念

通过引言可以知道,哈希表是为了解决数组越界这类问题的,它能够把值域比较大的数据映射到0~1e5或者1e6的范围。

2.1 概念

引用算法笔记上的原话:hash将元素通过一个函数转换为整数,使得该整数可以尽量唯一地代表这个元素。其中,这个转换函数称为散列函数H,即元素转换前为a,那么转换后的值为H(a)

2.2 方法

一般来说哈希函数直接取模就可以了,但是可能会产生冲突,把两个不一样的数映射成了同一个数。
如何处理冲突?分为下列两个方法:

2.2.1 开放寻址法

只需要一个一维数组,类似上厕所找坑位,如果找的这个坑有人了就看下一个,没人就占坑
在这里插入图片描述

  1. 添加
  2. 查找
  3. 删除(找到之后打一个标记)
#include<bits/stdc++.h>
using namespace std;

const int N = 2e5+3;//N是大于数据范围的第一个质数
int h[N],n;
const int INF = 0x3f3f3f3f;//INF不在数据范围内,用来初始化
int find(int x)
{
    //如果x在hash表中已经存在,返回x在hash表中的位置
    //如果不存在,返回x在hash表中可以占的第一个位置
    int k = (x%N+N)%N;
    while(h[k]!=INF&&h[k]!=x)
    {
        k++;
        if(k==N) k = 0;
    }
    return k;
}
int main()
{
    cin>>n;
    memset(h,INF,sizeof h);
    while(n--)
    {
        char op;
        cin>>op;
        int x;
        cin>>x;
        int k = find(x);
        if(op=='I')
        {
            h[k] = x;
        }
        else
        {
            if(h[k]!=INF) puts("Yes");
            else puts("No");
        }
    }
}

2.2.2 拉链法

很简单的方法:

  1. 开一个一维数组存储所有的哈希值
  2. 在每一个数组元素上拉一条链存储映射到这个元素上的所有数
  3. 在这里插入图片描述
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+3;//大于100000的第一个质数是100003
//这里有点像邻接表
int h[N],e[N],ne[N],idx;
void insert(int x)
{
    int k = (x%N+N)%N;//为什么+N%N,目的是让k为正数
    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;i = ne[i])
    {
        if(e[i]==x) return true;
    }
    return false;
}
int main()
{
    memset(h,-1,sizeof h);
    int n;
    cin>>n;
    while(n--)
    {
        char op;
        cin>>op;
        int x;cin>>x;
        if(op=='I')
        {
            insert(x);
        }
        else
        {
            if(find(x))
            {
                cout<<"Yes\n";
            }
            else
            {
                cout<<"No\n";
            }
        }
    }
}

三、字符串哈希

3.1 字符串前缀哈希法

预处理出所有前缀的哈希值:
在这里插入图片描述
如何定义某一个前缀的哈希值?

  1. 把字符串看成是一个p进制的数
  2. 比如ABCD,把A当成1,B当成2,C当成3,D当成4,那么 ( A B C D ) p = ( 1234 ) p = ( 1 ∗ p 3 + 2 ∗ p 2 + 3 ∗ p 1 + 4 ∗ p 0 ) m o d Q (ABCD)_p = (1234)_p = \\(1*p^3+2*p^2+3*p^1+4*p^0) mod Q (ABCD)p=(1234)p=(1p3+2p2+3p1+4p0)modQ因为可能式子值比较大,所以要取模。

通过上述方式即可以把任何一个字符串映射到从0到Q-1的任何一个数。
注意:

  1. 不能把字母映射成0,假设把A映射成0,而p进制下0是0,十进制下也是0,所以A是0,AA也是0,就会产生冲突。

  2. 字符串前缀哈希法依然可能存在冲突,使冲突概率最小的方法有个经验值:
    P = 131或13331,Q = 2的64次方

#include<bits/stdc++.h>
using namespace std;

const int N = 1e5+5,P = 131;
typedef unsigned long long ULL;
ULL h[N],p[N];
int n,m;
string x;
ULL query(int l,int r)
{
    return h[r] - h[l-1]*p[r-l+1];
}
int main()
{
    cin>>n>>m;
    cin>>x;
    p[0] = 1,h[0] = 0;
    for(int i= 0;i<n;i++)
    {
        p[i+1] = p[i]*P;
        h[i+1] = h[i]*P+x[i];
    }
    while(m--)
    {
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(query(l1,r1)==query(l2,r2)) cout<<"Yes\n";
        else cout<<"No\n";
    }
    return 0;
}

四、树哈希

未完待续~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值