哈希与哈希表

摘要

哈希算法是通过一个哈希函数 H,将一种数据(字符串、大数等)转化为能够用变量表示或者能直接作为数组下标的数,通过哈希函数转化得到的数值我们成为哈希值。通过哈希值可以实现快速查找和匹配。本文主要介绍两种哈希算法的应用:字符串 Hash 和哈希表。

字符串Hash

字符串Hash有几种不同的用途,但是基于同一个原理,就是通过hash值匹配。
首先就是模式串匹配问题,n = |S| , m = |T| ,可以通过维护原串中所有长度为 m 的子串的哈希值,实现在O(1)时间内转移,并在O(1)时间内与 T 串的 hash值判等,若相等则有可能匹配成功。
大部分模式串匹配问题是用KMP求解,但是如果要是从主串中每次选出两个子串判断是否匹配的问题,还是需要用字符串 Hash 求解。

具体流程
我们设计的哈希函数 H 应当能在O(1)时间内实现相邻子串间的转移,或在预处理后,对不同的子串应该能在O(1)时间内求出其哈希值;这里用到叫做 滚动哈希 的优化技巧。

我们选取两个合适的互质的常数 b 和 h(b < h),假设字符串 C = c 1 c 2 . . . c m C = c_1c_2...c_m C=c1c2...cm,那么我们定义函数: H ( C ) = ( c 1 b m − 1 + c 2 b m − 2 + . . . + c m b 0 )   m o d   h H(C) = (c_1b^{m-1} + c_2b^{m-2} + ... + c_mb^0)\:mod\:h H(C)=(c1bm1+c2bm2+...+cmb0)modh

这里的b是基数,相当于把字符串看作是 b 进制数。
这一过程是递推计算的,设H(C , k+1)是前 k 个字符构成的字符串的哈希值,则(不考虑取模): H ( C , k + 1 ) = H ( C , k ) ∗ b + c k + 1 H(C , k+1) = H(C, k) * b + c_{k+1} H(C,k+1)=H(C,k)b+ck+1

如果我们要求 C 中从 k 出发长度为 m 的子串的哈希值,那么 h s h = H ( C , k + m ) − H ( C , k ) ∗ b m hsh = H(C , k+m) - H(C, k) * b^m hsh=H(C,k+m)H(C,k)bm,hsh即为所求。

预处理

我们通过预处理求出所有C[a1, k](k <= n)的哈希值并存储在hash数组中(hash[i] = H(C,i) ),并预处理 base 数组(base[i] = b i b^i bi),那么我们就可以在O(1)时间内求出任意一个子串的哈希值。

在实现算法时,我们通常利用32位或者64位无符号整数计算哈希值,并取 h = 2 32 h = 2^{32} h=232 h = 2 64 h = 2^{64} h=264,通过自然溢出省去取模运算。通常 b 取131 或 13331时效果较好。

代码实现

/* bse[i] = b^i , hsh[i] = H(str, i) */
bse[0] = 1;//bse和hsh都是unsigned long long 
for(int i = 1;i <= n;i++){
    hsh[i] = hsh[i-1]*b + str[i]-'a';
    bse[i] = bse[i-1]*b;
} 

正确性证明
上述做法可以保证相同的字符串所产生的哈希值一定是相同的,但是不同的字符串所产生的哈希值一定是不同的吗?
并不一定,但是冲突的几率很小,我们通常认为算法竞赛不会出现不同字符串哈希值冲突的情况。实际上根据生日悖论,对于哈希值在[0 , n)内均匀分布的哈希函数,出现不同字符串哈希值相等的期望步数是 O ( N ) O(\sqrt N) O(N ),可以作为一个参考。
更进一步的,我们还可以使用 “双哈希” 降低冲突的概率,即取用不同的模数,把不同的模数算出的哈希值记下来,只有几个哈希值都一样,才判定字符串匹配。我们通常用双哈希就可以将冲突的概率降到很低,如果分别取 h = 1e9+7 和 h = 1e9+9,就几乎不可能发生冲突,因为它们是一对“孪生素数”。

哈希表与哈希函数

哈希表是一种高效的数据结构,查找时间效率是常数时间,同时也很容易实现,需要付出的代价是消耗内存,但在如今这点内存已不成问题。

问题模型
给定 n 个数,这些数可能很大,判断每个数在之前有无出现过,出现了几次?

算法流程
这里具体分为三步:构造哈希函数 H();将元素映射到哈希表;解决冲突。
首先要解决存储问题,我们先用线性表,即一维数组来存放元素。对于每个元素val,key = H(val)(这里的 key 是小于 1e7 的,可以当作数组的下标),于是我们就将val存放在数组下标为 key 的位置上。

而不同的元素它们key值是不同的(理想情况),相同的元素key值一定是相同的(任何情况),于是我们就可以通过O(1)时间的H()转化,读取数组内对应位置上的元素。

为了减少冲突呢,我们必须构造一个好的哈希函数H,这在后面会单独介绍常用的构造方法。但是不管多么优秀的哈希函数,都不可避免存在冲突,因此我们还需要有解决冲突的对策。我们想到可以用链表来解决冲突,即该一维表的每一个位置都是一条链表,具有相同哈希值的元素都放在同一条链上,当查找时只需要遍历这条链即可。这就叫哈希(链)表。

哈希函数的构造

哈希函数是决定哈希表查找效率的关键,只有哈希值分配的足够均匀时,单词查找的复杂度才会尽量小。以下介绍几种效果好的、容易实现的哈希函数。
(1)基数转换法
基数转换法就是开头的字符串Hash所采用的转换方法:将 val 值看作另一种进制数,然后再把它转化成对应的十进制数,再用除余法对其取余。一般取大于10的数作为转换的基数,并且两个基数是互质的。一般来说,取 131 或 13331较好。

如 val = 236075 原本是十进制数,现在将它看作十三进制数 ( 236075 ) 13 (236075) _ {13} (236075)13然后再将它转换为十进制数。
( 236075 ) 13 = 2 ∗ 1 3 5 + 3 ∗ 1 3 4 + 6 ∗ 1 3 3 + 7 ∗ 13 + 5 = ( 841547 ) 10 (236075)_ {13} = 2 * 13^5 + 3 * 13^4 + 6 * 13^3 + 7 * 13 + 5 = (841547) _ {10} (236075)13=2135+3134+6133+713+5=(841547)10

(2)除余法
选择一个适当的正整数 b,用其对 b 取模的余数作为哈希值,即:H(val) = val mod b,这个方法应用的最多,并且多数情况下性价比也是最高的。关键在于 b 的选取,一般选 b 是数组下标能存储得下并且尽量大的质数(一般根据空间取1e6左右的质数)。选质数是因为 通常b的约数越多,冲突的几率就越大。

(3)乘积取整法
我们用值 val 乘以一个在(0,1)中的实数 A(最好是无理数, ( 5 − 1 ) / 2 (\sqrt 5 - 1)/2 (5 1)/2是一个实际效果很好的数),得到一个(0 , k)之间实数;取其小数部分,乘以哈希表的大小 M 再向下取整,即得 val 在Hash表中的位置。函数表达式可以写成:H(val) = { M( val * A mod 1 ) }。

散列表模板

例题:门票(tickets)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 3e6+7;
const int P = 2181271; 
int nex[N],num[N],head[N],tot = 1;	// 用邻接表方式实现哈希表 
int top , stk[N];	//数组模拟堆栈,用于哈希表的初始化 
ll a,b,c;	
void init(){
	tot = 0; 
	while(top) head[stk[top--]] = 0;//只清空用了的表,节省时间 
} 
void Insert(ll x){
	/*将x插入哈希表*/
	int h = x % P;
	for(int i = head[h] ;i ;i = nex[i]){
		if(num[i] == x) return;	 //相同的值已经存放过了 
	} 
	if(!head[h]) stk[++top] = h; //第一次出现的哈希值入栈
	/* 邻接表添加元素基本套路 */
	nex[++tot] = head[h]; head[h] = tot; num[tot] = x; 
}
bool ask(ll x){
	/* 返回 x 是否存在*/
	int h = x%P;
	for(int i = head[h];i;i = nex[i]){
		if(num[i] == x) return true;
	}
	return false;
}
int solve(){
	ll x = 1; Insert(1);
	for(int i = 1;i < 2e6;i++){
		x = (x*a + x%b)%c;
		if(ask(x)) return i;
		Insert(x); 
	} 
	return -1;
}
int main(){
	/*多组样例要调用init()*/
	scanf("%lld%lld%lld",&a,&b,&c);
	printf("%d\n",solve());
	return 0;
}

参考资料

  • 董永建,信息学竞赛一本通提高版,福州:福建教育出版社,2018.6,63-69
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页