子字符串查找之Rabin-Karp算法

版权声明:个人笔记,仅供复习 https://blog.csdn.net/weixin_41162823/article/details/85550715

1.背景

1.1 算法简介:M.O.Rabin和R.A.Karp发明了一种基于散列的字符串查找算法。我们只需要计算模式串的散列函数,然后利用相同的散列函数计算文本中所有可能的M个字符的子字符串散列值并寻找匹配。如果找到了一个散列值和模式字符串相同的子字符串,再继续验证是否相同。这是一个有趣的算法,重点不在于其只用线性时间解决问题,而在于其对散列技术的使用,这是一个具有启发性的算法!

1.2 结论:Rabin-Karp算法优点是运行速度为线性级别,缺点是内循环很长。(若干次算术运算,而其他算法都只需比较字符)

2.思想

2.1 基本思想:长度为M的模式串对应着一个R进制的M位数使用除留余数法构造一个能够将 R 进制的 M 位数转化为一个0到Q-1之间的 int 值的散列函数。我们在不溢出的情况下选择一个尽可能大的随机素数Q(因为我们并不真正需要一张散列表,故Q越大越好,冲突越少)。接下来对文本所有长度为M的子串计算散列值并寻找匹配。

2.2 计算散列函数:思想很简单,只需将所有子串散列值求出,一个一个匹配即可,但是如果离线计算每个子串的散列值,时间复杂度与 M*N 成正比,故问题变成了如何在线性时间求出散列值。

2.3 关键思想:既然子串长度确定为M(即模式串的长度M),那我们每次只需向后移动一位,将第一个元素去掉,最后一个元素加上即可更新散列值,再与模式串散列值比较即可。在这种情况下时间复杂度与N成正比。

3.实现

3.1 具体步骤:

  1. 得到模式串长度M
  2. 选择一个恰当的R,尽可能大的随机素数Q,构造散列函数
  3. 计算出模式串的散列值patHash
  4. 从文本第一个子串开始,依次向右移动,判断其散列值是否与patHash相等

3.2 利用蒙特卡洛法验证正确性:在之前我们讲过,当散列值相同时我们再逐个比较字符是否匹配,以此来确保我们得到的是一个匹配而非仅仅散列值相同的子串,但是我们可以不这么做。假设我们取一个1e20数量级的素数Q,那么一个随机键的散列值与模式串冲突的概率就会小于1e-20。这足以确保答案的正确性,如果仍不放心,我们可以再运行一次,这样就下降到了1e-40,同理你可以将概率降到你满意的数值(牺牲时间)。

3.3 代码示例:

/*
Rabin-Karp算法
-ValenShi 
只能匹配数字,选R为10,若想匹配其他字符,需要调整。 
*/ 
#include<cstdio>
#include<cstring>
typedef long long ll;
const int maxn = 1e5;
const int R = 10;
const int Q = 1e9+7;
char pat[maxn],txt[maxn];
int RM;
ll qpow(ll a,ll b,ll M){
	ll res = 1;
	while(b){
		if(b&1)	res = res*a%M;
		a = a*a%M;
		b >>= 1;
	}
	return res%M;
}
int charValue(char a[],int i){
	return a[i]-'0';		//仅适用于数字 
}
ll hash(char a[],int m){
	ll res = 0;
	for(int i = 0;i < m;i++)
		res = res*R + charValue(a,i);
	return res;
}
bool check(int x){
	return true;	//可在此对每个字符进行匹配 
}
int solve(){
	scanf("%s",txt);
	scanf("%s",pat);
	int m = strlen(pat);
	int n = strlen(txt);
	RM = qpow(R,m-1,Q);
	ll patHash = hash(pat,m);
	ll txtHash = hash(txt,m);
	if(txtHash == patHash)	return 0;
	for(int i = 0;i < n-m;i++){
		txtHash = (txtHash - RM*charValue(txt,i)%Q + Q)%Q;
		txtHash = (txtHash*R + charValue(txt,i+m))%Q;
		if(patHash == txtHash)
		if(check(i-m+1))	return i+1;
	} 
	return n;				//未找到 
}
int main(){
	printf("%d",solve());
	return 0;
}

3.4 使用注意:与KMP比起来,这个算法理解起来简单一些(如果熟悉散列表的话),也有趣一些,但是该算法需要注意R的选取。如果仅仅匹配数字字符串,那么R大于10皆可,若包括各种字符,我们则需要为每个字符设定一个R进制的值(可以使用ASCII码),这时要求R要大于字符总数(否则有冲突)。相比之下KMP算法等其他算法则没这方面的缺陷。

参考资料:

《算法导论》原书第三版 P580

《算法》第四版 [Robert Sedgewick] P505

猜你喜欢

转载自blog.csdn.net/weixin_41162823/article/details/85550715