简单の暑假总结——哈希

3.1 哈希

哈希,是一种神奇的数据结构。

其实就是手打 map

假设现有一数组 A A A ,你需要找到某个元素在数组中的位置,一般情况下,我们会采用线性筛,即用一层循环找到该元素,时间复杂度为 O ( n ) O(n) O(n)

当然,在 A A A 数组有序的情况下,我们可以采用二分法,快速的找到该元素,时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

那他要是无序呢?

那么,想要在无序的数组中快速找到某个元素,我们就可以运用哈希!

所谓哈希,其实就是将每一个元素通过一定方式(即哈希函数),映像到一个区间中,并以该元素在区间上的作为记录在表中的存储位置,这个表叫做哈希表。上述的整个过程叫哈希造表或散列,所得的存储位置叫哈哈希地址散列地址

当然,我们不能保证每一个元素的哈希地址绝对是唯一的

例如,我们的哈希函数是 H a s h (   i   ) = i ÷ 3 + 1 Hash(\ i\ )=i \div 3+1 Hash( i )=i÷3+1 那么,我们分别将 1 , 2 1,2 1,2 代入 i i i 中,我们会发现: H a s h (   1   ) = 1 ÷ 3 + 1 = 1 Hash(\ 1\ )=1 \div 3+1=1 Hash( 1 )=1÷3+1=1 H a s h (   2   ) = 2 ÷ 3 + 1 = 1 Hash(\ 2\ )=2 \div 3+1=1 Hash( 2 )=2÷3+1=1

1 1 1 2 2 2 的哈希地址是相同的!

这时,我们就称这种情况为冲突

在哈希中,冲突不可避免,但是可以减少次数

一般来说,冲突与如下方面有关:

  1. 装填因子 α \alpha α
  2. 哈希函数
  3. 解决冲突的办法

第二点和第三点很好理解,解释一下第一点:

装填因子是指哈希表中需要存入的元素个数 n n n哈希表的大小 m m m 的比值,即 α = n ÷ m \alpha=n \div m α=n÷m

显然,当 α \alpha α 越小,越不容易发生冲突;当 α \alpha α 越大,越容易发生冲突:

在这里插入图片描述
在这里插入图片描述

当然,如果 α \alpha α 过小,会导致空间的浪费,需要权衡。

接下来,我们思考一下哈希函数

3.2 哈希函数

哈希函数构造方法多样,下面介绍一些流行的和不流行的(神奇的废话文学

3.2.1 直接定址法

直接定址法比较简单,直接欣赏一下其哈希函数: H a s h (   i   ) = a i + b Hash(\ i\ )=ai+b Hash( i )=ai+b H a s h (   i   ) = i ÷ a + b Hash(\ i\ )=i\div a+b Hash( i )=i÷a+b

显然,在 i i i 无重复的情况下,我们是绝对可以避免冲突的发生的,然而,直接定址法在面对一些跨幅较大的数据时,浪费空间。

3.2.2 除后余数法

除后余数法比直接定址法更简单。对于一个长度为 m m m 的哈希表,其哈希函数: H a s h (   i   ) = i   m o d   p ( p ⩽ m ) Hash(\ i\ )=i\bmod p(p\leqslant m) Hash( i )=imodp(pm)

显然,如果 p > m p>m p>m 那么求得的 H a s h (   i   ) Hash(\ i\ ) Hash( i ) 可能大于 m m m ,进而导致数组越界。(RE达咩

3.2.3 平方取中法

对于元素 i i i ,可以将 i 2 i^2 i2 中间几位作为哈希地址。(为啥子呢?)

假设 i = a b c d ‾ i=\overline{abcd} i=abcd

i 2 = a b c d ‾ 2 = ( 1000 a + 100 b + 10 c + d ) 2 = 1000000 a 2 + 100000 a b + 10000 a c + 1000 a d + 100000 a b + 10000 b 2 + 1000 b c + 100 b d + 10000 a c + 1000 b c + 100 c 2 + 10 c d + 1000 a d + 100 b d + 10 c d + d 2 = 1000000 a 2 + 100000 × ( 2 a b ) + 10000 × ( 2 a c + b 2 ) + 1000 × ( 2 a d + 2 b c ) + 100 × ( 2 b d + c 2 ) + 10 × ( 2 c d ) + d 2 i^2=\overline{abcd}^2=(1000a+100b+10c+d)^2=1000000a^2+100000ab+10000ac+1000ad+100000ab+10000b^2+1000bc+100bd+10000ac+1000bc+100c^2+10cd+1000ad+100bd+10cd+d^2=1000000a^2+100000\times(2ab)+10000\times(2ac+b^2)+1000\times(2ad+2bc)+100\times(2bd+c^2)+10\times(2cd)+d^2 i2=abcd2=(1000a+100b+10c+d)2=1000000a2+100000ab+10000ac+1000ad+100000ab+10000b2+1000bc+100bd+10000ac+1000bc+100c2+10cd+1000ad+100bd+10cd+d2=1000000a2+100000×(2ab)+10000×(2ac+b2)+1000×(2ad+2bc)+100×(2bd+c2)+10×(2cd)+d2

显然,中间的三都与 a , b , c , d a,b,c,d a,b,c,d 有关,唯一性高,自然可以用作哈希地址。

3.2.4 数字分析法

当元素 i i i 与元素 j j j部分数位上的数相同的频率较大时,我们可以将其它数位上的数摘下来作为哈希地址,即数字分析法。

举个例子:

第一位 第二位 第三位 第四位 第五位 第六位 1 1 4 5 1 4 1 1 5 0 4 8 1 1 4 4 1 2 1 1 4 9 9 6 1 1 5 2 3 5 1 1 5 1 0 0 \begin{array}{|c|c|c|c|c|c|}第一位&第二位&第三位&第四位&第五位&第六位\\1&1&4&5&1&4\\1&1&5&0&4&8\\1&1&4&4&1&2\\1&1&4&9&9&6\\1&1&5&2&3&5\\1&1&5&1&0&0\end{array} 第一位111111第二位111111第三位454455第四位504921第五位141930第六位482650

我们现在有六个数: 114514 , 115048 , 114412 , 114996 , 115100 114514,115048,114412,114996,115100 114514,115048,114412,114996,115100 ,如上表所示。我们发现:第一位和第二位都是 1 1 1 ,第三位要么是 4 4 4 要么是 5 5 5 ,而剩下的三位相对混乱,数字分布均匀,所以我们可以将其作为哈希地址,分别为 514 , 48 , 412 , 996 , 100 514,48,412,996,100 514,48,412,996,100

3.2.5 折叠法

我们将元素 i i i 按要求的长度分成位数相等的几段(最后一段可以略短),然后把各段重叠在一起相加,以所得的和作为地址。

适用于:每一位上各符号出现概率大致相同的情况。——PPT

不知道各位读者理不理解,反正我不理解为啥适用于【每一位上各符号出现概率大致相同的情况】,反正也不常用,理不理解倒也无所谓,如果有理解的也欢迎讨论!——蒟蒻

举个栗子(栗子:为啥老举我

现在,我们有 i = 422759 i=422759 i=422759 ,那么,我们可以将其分为三段: 42 , 27 , 59 42,27,59 42,27,59 。将它们相加: 42 + 27 + 59 = 128 42+27+59=128 42+27+59=128 。我们就求到了 H a s h (   i   ) = 128 Hash(\ i\ )=128 Hash( i )=128

3.2.5.1 间接叠加法

间接叠加法是折叠法的孪生兄弟,它在分段的基础上要将每一个奇数段进行反转

在上面这个例子中, i i i 的哈希地址就应该是 H a s h (   i   ) = 24 + 27 + 95 = 146 Hash(\ i\ )=24+27+95=146 Hash( i )=24+27+95=146

搞这么复杂,谁用啊

3.2.6 随机数法

将一个随机数作为 i i i 的哈希地址,即 H a s h (   i   ) = r a n d (   i   ) Hash(\ i\ )=rand(\ i\ ) Hash( i )=rand( i )

最简单,同时也很常用。

扩展小芝士

我们在采用随机数法时,得到的数是伪随机数,是计算机通过算法算出来的!

我们在生成随机数时,首先,需要给计算机一个种子,计算机根据种子,通过算法,将其对应的伪随机数算出来。

注意:对于相同的种子,算出来的伪随机数一定是相同的!

但是,在实际操作中,我们不会手动输入种子,这时,我们需要一个随时变化的量作为种子,比如时间

那么,我们就可以得到生成随机数的代码了:

#include<ctime>            //打开time函数 
#include<cstdio>
#include<cstdlib>			//打开rand函数 
int main(){
	srand((unsigned)time(NULL));			//或者srand(time(0))
	printf("%d",rand());
	return 0;
}

那么,对于哈希函数的选取,我们一般从如下 5 5 5 个方面进行考虑:

  1. 计算哈希函数所需时间(包括硬件指令的因素)
  2. 关键字的长度
  3. 哈希表的大小
  4. 关键字的分布情况
  5. 记录的查找频率

对于不同的题目,哈希函数也有所不同,具体情况具体分析。

3.3 处理冲突

前文已提,冲突是不可避免的,有了冲突不可怕,可怕的是不会处理冲突

下面介绍一些处理冲突的方法

3.3.1 开放地址法

开放地址法就是寻找哈希表中未被占用的哈希地址,并让有冲突的元素塞进去

我们一般使用两种办法来寻找未被占用的哈希地址

3.3.1.1 线性探测法

线性探测法,顾名思义,我们以此遍历哈希表中的每一个哈希地址,直至找到未被占用的哈希地址,并让有冲突的元素塞进去

具体来说,设哈希函数为 H a s h (   i   ) = i   m o d   m Hash(\ i\ )=i\bmod m Hash( i )=imodm 那么,我们对应的第 i i i 次计算所得的哈希地址为 H i = ( H a s h (   i   ) + d i )   m o d   m   ( d i = i ) H_i=(Hash(\ i\ )+d_i)\bmod m\ (d_i=i) Hi=(Hash( i )+di)modm (di=i)

对于线性探测法,只要哈希表中还有空的哈希地址,那么,我们就一定找得到一个未被占用的哈希地址

但是,考虑如下一个最坏情况:

原本存在第 i i i 号哈希地址的元素因冲突存到了第 i + 1 i+1 i+1 号哈希地址,原本存在第 i + 1 i+1 i+1 号哈希地址的元素因冲突存到了第 i + 2 i+2 i+2 号哈希地址,原本存在第 i + 2 i+2 i+2 号哈希地址的元素因冲突存到了第 i + 3 i+3 i+3 号哈希地址……

这样下去,我们在查找时就大大的不方便(因为几乎所有的元素都是错位存放)!

所以,我们采用了新方法:二次探测法

3.3.1.2 二次探测法

二次探测法是一个小知识点,我们简单了解一下其计算公式:

设哈希函数为 H a s h (   i   ) = i   m o d   m Hash(\ i\ )=i\bmod m Hash( i )=imodm 那么,我们对应的第 i i i 次计算所得的哈希地址为 H i = ( H a s h (   i   ) + d i )   m o d   m H_i=(Hash(\ i\ )+d_i)\bmod m Hi=(Hash( i )+di)modm ,其中, d i d_i di 分别表示 1 2 , − 1 2 , 2 2 , − 2 2 , ⋯   , j 2 , − j 2 ( j ≤ m ÷ 2 ) 1^2,-1^2,2^2,-2^2,\cdots,j^2,-j^2(j\le m\div 2) 12,12,22,22,,j2,j2(jm÷2)

其实就是左摸一下,右摸一下,摸到为止

3.3.2 链地址法

顾名思义,我们把每一个哈希地址都看作一个链结构,每当我们要塞入一个元素时,我们只需要将该元素塞入对应的链当中,也就不需要处理冲突了。

但是呢,因为我们不知道冲突发生的次数,即链表的一个具体长度,所以,除非使用 vector ,不然,很容易造成空间浪费

3.3.3 再哈希法

其实很简单,一种哈希算法不行,就换一种哈希算法接着搞,搞到一个未被占用的哈希地址为止

说实话,我觉得这种算法非常浪费时间(虽然很好想,但是很暴力

3.3.4 公共溢出区法

当我们发现当前要塞入的元素与原有的元素产生了冲突,那么,我们就不把它塞进哈希表里,而是把它塞进一个公共溢出区里

这样,原哈希表里装着的就是“原住民”,而公共溢出区里装着的就是起了冲突的元素

3.4 字符串哈希

考虑到接下来的例题与字符串哈希有关,这里简单的介绍一下:

我们采用的是字母转化法名字是我瞎编的

简单来说,我们把每一个字母映射成一个数字,即 a → 1 , b → 2 , c → 3 , ⋯   , z → 26 a \rightarrow 1,b \rightarrow 2,c \rightarrow 3,\cdots,z\rightarrow 26 a1,b2,c3,,z26

然后把他们转化成一个26进制的一个数字

严格来说,应该是按照26进制的计算方法将其转化成一个10进制数,因为两个不同的字符串具有唯一性,而最终算出来的结果也具有唯一性,即没有冲突的发生

那么,具体来说:我们有一个字符串 a b c abc abc ,按照如上的说法,他就应该等于 1 × 2 6 2 + 2 × 2 6 1 + 3 × 2 6 0 = 731 1\times 26^2+2\times 26^1+3\times 26^0=731 1×262+2×261+3×260=731 ,也就是说, a b c abc abc 被存储到了哈希表中的第 731 731 731 号哈希地址

还是 map 香

那么如果我们要求字符串当中字串的哈希值呢?

我们拿整数 123456 123456 123456 举个例子

我们要从 123456 123456 123456 节选出 456 456 456 ,我们可以用 123456 − 123 × 1000 123456-123\times1000 123456123×1000 来达到这一目的

其中, 1000 1000 1000 0 0 0 的个数恰好是 456 456 456 的位数

由此启发,我们可以通过类似的方法,通过将一个大的子串的哈希值 − - 不包含所求子串的子串的哈希值 × \times × 对应位数的进制就可以得到所求子串的哈希值了

3.5 小例题

Eg 子串查找

模板,只需要判断字符串 a a a 长度为 s i z e ( b ) size(b) size(b) 的子串中,有多少个字串的哈希值与 b b b 的哈希值即可

所以在考场上那些人是怎么错的?

#include<cstdio>
#include<cstring>
char a[1000005],b[1000005];
long long int hash1[1000005],hash2[1000005],Pow[1000005],ans;
//hash1 存储 a 字符串的哈希值,hash2 存储 b 字符串的哈希值,Pow 存储进位(简省时间复杂度)
int main(){
	scanf("%s%s",a+1,b+1);
	int len1=strlen(a+1),len2=strlen(b+1);
	for(int i=1;i<=len2;i++){
		hash1[i]=hash1[i-1]*131+b[i]-'A';
	}
	Pow[0]=1;
	for(int i=1;i<=len1;i++){
		hash2[i]=hash2[i-1]*131+a[i]-'A';
		Pow[i]=Pow[i-1]*131;
	} 
	//以上分别初始化 a 字符串的哈希值, b 字符串的哈希值以及对应进制
	for(int i=len2;i<=len1;i++){
		if(hash1[len2]==hash2[i]-hash2[i-len2]*Pow[len2]){			//上文已提,判断哈希值是否相等
			ans++;
		}
	}
	printf("%d",ans);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值