6. 哈希表

定义

######## 假设 x 的范围为 10-9 ~ 109;h(x) 的范围为 0 ~ 10-5
哈希表的实质即为将一个大规模的数映射给一个较小规模的数,从而方便操作(使用函数)
对于维护的hash函数,将大范围的数的范围作为定义域,所映射的值的范围作为值域

  1. 一般情况下用取模的方法来进行映射 x mod n(对于n,要取为质数,并离2的整次幂尽可能远,减少冲突)所以查找题目的数据的后的第一个质数
  2. 由于定义域较大,值域较小,所以一般会发生冲突(两个不同的数映射到了同一个值),所以就要用到开放寻址和拉链两种方法;来进行存储

存储结构

1. 拉链法

对于拉链法,即按照 h(x) 的范围先创建一个从1依次递增的一维数组,然后将所输入的x的值进行映射,映射到某一个 h(x) 的位置就在它的下面拉一条链存储数据(所拉的链为链表,动态存储)我原先想的多维数组 哈哈哈
插入:即先找出所插入x对应的 h(x) ,然后在一维数组的对应位置拉出一条链存储所插入的x
查找:也是先找到x对应的 h(x) 值,然后在一维数组中找到 h(x) 的位置并遍历其中的数看是否存在x
插入和查找都要用到的是链表的知识,先回顾一下:

  1. 链表的头节点插入:(在这道题中其实就相当于把h[] 上的值当作是头节点,所指向的即是第一个节点)
//将x插入到头节点的下一个
void add_to_head(int x) {
	e[idx] = x;	//将x存到idx的位置(赋值) 
	ne[idx] = head;	//将head的指向赋给idx
	head = idx;	//将head指向idx
	idx++;	//此处的idx已经用过,将idx移到下一个位置
}
  1. 链表的查找(链表中以-1作为空节点
//其实就相当于是一个遍历,若有相等值就输出true,否则就输出false
for (int i = head; i != -1; i = ne[i]) {
		if (e[i] == x)	return true;
		else return false;
	}

再补充一个有关求质数的算法:(以200000为例)

for (int i = 200000;; i++) {
		bool flag = true;
		for (int j = 2; j * j <= i; j++) {
			if (i % j == 0) {
				flag = false;
				break;
			}
		}

		if (flag) {
			cout << i;
			break;
		}
	}

题1:

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

I x,插入一个数 x;
Q x,询问数 x 是否在集合中出现过;
范围:1 ≤ N ≤ 105
			−109 ≤ x ≤ 109
#include<iostream>
#include<cstring>
using namespace std;

const int N = 100003;	//定义为所要开的数组大小后的第一个质数

int h[N];	//定义一维数组
int e[N], ne[N], idx;	//定义链表所需的结构

void insert(int x){
	int k = (x % N + N) % N;	//将输入的值进行映射,k为映射到的位置在一维数组中的下标, + N 的操作为防止出现负数

	//映射的插入,即链表的插入
	e[idx] = x;
	ne[idx] = h[k];	//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;
}

int main() {
	int n;
	cin >> n;

	memset(h, -1, sizeof h);	//对整个一维数组上的所有链进行初始化为-1,-1代表空节点

	while (n--) {
		char op[2];
		int x;
		cin >> op >> x;

		if (op[0] == 'I')	insert(x);
		else
		{
			if(find(x))	puts("Yes");
			else puts("No");
		}
	}

	return 0;
}

memset函数简介:
memset是一个初始化函数,作用是将某一块内存中的全部设置为指定的值。并且是按照字节来进行赋值,将每个字节都赋为所传入的值,char有一个字节,int有四个字节,注意在赋值时的对应值,并且每个字节为八位,在赋二进制时注意其前面的零的个数也要算进所赋的值,所以一般在用memset赋值时赋为 0 或 -1

void *memset(void *s, int c, size_t n); 

s指向要填充的内存块。
c是要被设置的值。
n是要被设置该值的字符数。
返回类型是一个指向存储区s的指针。
具体见:memset函数详解

2. 开放寻址法

对于开放寻址法,与拉链法相比,只维护一个一维数组,不加链表。
一维数组的长度一般为题目数据范围的2~3倍(开较大的原因即为防止坑位较少无返回值造成死循环)(然后求放大之后的数据范围后的第一个质数作为基数来取模 )
在处理冲突时,从所找到的点的位置往后找直至找到空的位置将所插入的数插入,若往后没有找到则从头开始
(题目同上)

#include<iostream>
#include<cstring>
using namespace std;

const int N = 200003;	//定义为所要开的数组大小后的第一个质数
const int null = 0x3f3f3f3f;	//定义一个不在x的数据范围之内的数来标记所查找的点并未使用

int h[N];	//定义一维数组

int find(int x){
	int k = (x % N + N) % N;

	while (h[k] != null && h[k] != x) {
		k++;	//先从k的位置往后循环进行查找
		if (k == N)	k = 0;	//当循环至最后时仍未找到,则要返回至最开始继续循环
	}

	return k;	//如果所查询的值在hash表中时返回其在hash表中的下标,如果不在hash表中时则返回其应在hash表中插入的位置,且该位置此时为初始值null
}

int main() {
	int n;
	cin >> n;

	memset(h, 0x3f, sizeof h);	//memset 函数,将数组中的所有值进行初始化,在此处初始化为0x3f即为将所有的位置都初始化为没有插入的形式,此外由于memset函数是按字节数来进行赋值,又int是有四个字节,所以会将int的每个字节都赋为3f,即为N所对应的0x3f3f3f3f

	while (n--) {
		char op[2];
		int x;
		cin >> op >> x;

		int k = find(x);
		if (op[0] == 'I')	h[k] = x;
		else
		{
			if (h[k] != null)	puts("Yes");	//当所查询的值对应的find返回值为应插入的地方即为null时代表未查询到,无该值
			else puts("No");
		}
	}

	return 0;
}

理解:

  1. 对于开放寻址法只需要维护一个一维数组不需要链表,在应用时只需要一个find查找函数(蹲坑法)
  2. 对于find函数:若所传入的值本应对应的位置为空,则直接返回此位置用于将所传入的值插入;若所传入的值本应对应的位置不为空但所存储的值刚好与所传入的值相同,则也直接返回该位置;若所传入的值本应对应的位置不为空,且所存储的值与所传入的值不同,则将k往后移动直至找到空位或者找到与所传入值相同的位置下标,若到数组末尾也仍没有找到满足条件的下标,则从数组的开始进行查找。所以对于find函数的返回值,若有与传入值相同的值,则返回所对应的位置,若没有则返回k往后的第一个空位(即所传入的值所应插入的地方),用这种往后找空的坑位的方法来解决冲突。(并且由于所开的坑位较多,两到三倍,所以一定会有返回值,一般不会坑位全满造成死循环,这就是为什么要开2~3倍)
  3. 对memset的解释,由于memset对数组进行初始化时是按照字节来进行赋值的,memset中传入 int 和 3f ,由于int有四个字节,会使每个字节都赋为 3f ,所以其实对数组中每个值的赋值都是0x3f3f3f3f。具体见上述链接博客
关于0x3f3f3f3f的使用:
在算法竞赛中,我们常常需要用到设置一个常量用来代表“无穷大”。

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

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

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

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

字符串哈希方式(字符串前缀哈希法)

主要用于快速判断两字符串是否相等的情况
注意:

  1. 字符串的前缀哈希方式即是取字符串的前缀来进行 P 进制的转化(与2进制相似)
  2. 不能将某个字符映射为0,eg:若把 a映射为0,则aaa也为0,则会有不同的字符串映射到同一值,发生冲突
  3. 根据经验值,P = 131 或 13331 ,Q = 2 ^ 64 时不存在冲突,所以不必再考虑冲突(此外,在此处Q = 2 ^ 64时可使用自然溢出法来进行mod,在定义数据类型时定义为undesigned long long ,数据大小范围恰好为2 ^ 64,所以若数据溢出时会自动将所输入的数据来 mod 2 ^ 64,即不需要再在进行mod操作)
  4. 字符串哈希的计算公式:
    对于字符串的哈希方式是将字符串的前缀依次分离出来依次存入h数组
    所以在求从1开始的哈希值直接求其在h数组中的下标的对应值即可
    但若要求中间两点之间(假设为L~R)的哈希值之差时则要,先将小标对应至与大标相同的p进制位数,即乘以P的R-L次方将其所有位数左移,然后用以R为下标的值来减去以L为下标的值转化后的值,即为所求哈希值之差。思想即为将两个P进制的值从左边对齐,然后再作差即为中间部分的P进制的值,即中间部分所对应的哈希值
**//大佬儿思路**
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1XnX1X2X3⋯Xn−1Xn 的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。

映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ(X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ
注意点:
1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
2. 冲突问题:通过巧妙设置P (131 或 13331) , Q (2^64)的值,一般可以理解为不产生冲突。

问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。
求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。

前缀和公式 h[i+1]=h[i]×P+s[i]h[i+1]=h[i]×P+s[i] i∈[0,n−1]i∈[0,n−1] h为前缀和数组,s为字符串数组
区间和公式 h[l,r]=h[r]−h[l−1]×Pr−l+1h[l,r]=h[r]−h[l−1]×Pr−l+1
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。

代码:

#include<iostream>
using namespace std;

typedef unsigned long long ULL;	//使用undesigned long long 来进行自然溢出,不必再进行mod

const int N = 100010, P = 131;

int n, m;
char str[N];
ULL h[N];	//h[i]为前i个字符的哈希值(即为对应的P进制的值)
ULL p[N];	//p[]中所存储的是每一位在P进制中所对应的权重,即p[i] = P ^ i

//定义get函数求前缀为l和前缀为r之间的字符串所对应的哈希值
ULL get(int l, int r) {
	return h[r] - h[l - 1] * p[r - l + 1];
}

int main() {
	cin >> n >> m;
	cin >> (str + 1);

	p[0] = 1, h[0] = 0;
	//定义p[]和h[]数组
	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;
}

tips:注意一点就是在例代码中我是用的char来进行输入字符串,用 str+1 即可使下标从1开始,但是若是要用string类型,则在输入时无法改变下标从1开始,默认为从0开始,只能是在输出时用下标 -1 来对应

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值