小白学算法(14)哈希表

1,018 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第18天,点击查看活动详情

哈希表(散列表)

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

1.模板介绍

1)拉链法

通过哈希函数得到的哈希值可能会有冲突的,那么就将冲突的数形成一个链表,如下图。

img

//h 哈希数组
//e ne 当前位的拉链(前面静态数组模拟链表)
//idx 标识链表节点数  参考前面静态数组模拟链表
int h[N], e[N], ne[N], idx;
// 向哈希表中插入一个数
void insert(int x)
{
    //N最好是质数,那么哈希函数取得值冲突就越小(数学证明)
    //x%N 可能x为负数 +N %N 是为了让哈希结果为正数
    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;
}

2)开放寻址法

就是哈希函数得到的值没位置了,就向前开个位置。(厕所蹲坑,没位置移旁边去)


    N的大小要开到 数据范围的2-3int h[N];
    const int null=0x3f3f3f3f;
    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    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;
    }

5.jpg

3)实操演练

拉链法

840. 模拟散列表 - AcWing题库

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

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

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

#include<iostream>
#include<cstring>
using namespace std;
const int N=1e5+3;
int h[N],e[N],ne[N],idx;
void insert(int x){xx}
bool query(int x){xx}
int main()
{
    int n;
    cin>>n;
    memset(h,-1,sizeof h);
    while(n--)
    {
        string c;
        int x;
        cin>>c>>x;
        if(c=="I")
        {
            insert(x);
        }else
        {
            bool result=query(x);
            if(result)
            {
                cout<<"Yes"<<endl;
            }else
            {
                cout<<"No"<<endl;
            }
        }
    }
}
开放寻址法
#include<iostream>
#include<cstring>
using namespace std;
const int N=2e5+3;
const int null=0x3f3f3f3f;
int h[N];
int query(int x){xxx}
int main()
{
    int n;
    cin>>n;
    memset(h,0x3f,sizeof h);
    while(n--)
    {
        char op;
        int x;
        cin>>op>>x;
        if(op=='I')
        {
            h[query(x)]=x;
        }else
        {
            if(h[query(x)]!=null)
            {
                cout<<"Yes"<<endl;
            }else
            {
                cout<<"No"<<endl;
            }
        }
    }
    return 0;
}

4)解释问题

引用AcWing 840. 基础_哈希表_模拟散列表_开放寻址法细节 - AcWing

const int null = 0x3f3f3f3f 和 memset(h, 0x3f, sizeof h)之间的关系;

首先,必须要清楚memset函数到底是如何工作的
先考虑一个问题,为什么memset初始化比循环更快?
答案:memset更快,为什么?因为memset是直接对内存进行操作。memset是按字节(byte)进行复制的

void * memset(void *_Dst,int _Val,size_t _Size);
这是memset的函数声明
第一个参数为一个指针,即要进行初始化的首地址
第二个参数是初始化值,注意,并不是直接把这个值赋给一个数组单元(对int来说不是这样)
第三个参数是要初始化首地址后多少个字节
看到第二个参数和第三个参数,是不是又感觉了
h是int类型,其为个字节, 第二个参数0x3f八位为一个字节,所以0x3f * 4(从高到低复制4份) = 0x3f3f3f3f

这也说明了为什么在memset中不设置除了-10以外常见的值
比如1, 字节表示为00000001memset(h, 1, 4)则表示为0x01010101

为什么要取0x3f3f3f,为什么不直接定义无穷大INF = 0x7fffffff,即32个1来初始化呢?


 3.1 首先,0x3f3f3f的体验感很好,0x3f3f3f3f的十进制是1061109567,也就是10^9级别的
     (和0x7fffffff一个数量级),而一般场合下的数据都是小于10^9的,所以它可以作为无穷大
     使用而不致出现数据大于无穷大的情形。
     比如0x3f3f3f3f+0x3f3f3f3f=2122219134,这非常大但却没有超过32-bit,int的表示范围,
     所以0x3f3f3f3f还满足了我们“无穷大加无穷大还是无穷大”的需求。
     但是INF不同,一旦加上某个值,很容易上溢,数值有可能转成负数,有兴趣的小伙伴可以去试一试。

 3.2 0x3f3f3f3f还能给我们带来一个意想不到的额外好处:如果我们想要将某个数组清零,
     我们通常会使用memset(a,0,sizeof(a))这样的代码来实现(方便而高效),但是当我们想
     将某个数组全部赋值为无穷大时(例如解决图论问题时邻接矩阵的初始化),就不能使用
     memset函数而得自己写循环了(写这些不重要的代码真的很痛苦),我们知道这是因为memset
     是按字节操作的,它能够对数组清零是因为0的每个字节都是0,
     现在如果我们将无穷大设为0x3f3f3f3f,那么奇迹就发生了,0x3f3f3f3f的每个字节都是0x3f!所以
     要把一段内存全部置为无穷大,我们只需要memset(a,0x3f,sizeof(a))。
 memset(a,-1,sizeof h)

2.字符串的哈希

841. 字符串哈希 - AcWing题库

给定一个长度为 nn 的字符串,再给定 mm 个询问,每个询问包含四个整数 l1,r1,l2,r2l1,r1,l2,r2,请你判断 [l1,r1][l1,r1] 和 [l2,r2][l2,r2] 这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

核心思想:将字符串看成P进制数,P的经验值是13113331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
    //不能将字母映射为0,因为0的各个进制都是0,会导致一些不同的字段为相同哈希值了

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值

区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P2P2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}