原文链接:http://blog.youkuaiyun.com/onezeros/article/details/5531354
Karp Rabin 算法是利用hash函数的特性进行字符串匹配的。
KR算法对模式串和循环中每一次要匹配的子串按一定的hash函数求值,如果hash值相同,才进一步比较这两个串是否真正相等
也许你会有这样的疑问,在Brute force暴力匹配中,每一次都把模式串和文本当前字串匹配,现在每一次都计算hash,还要进一步比较,会不会更慢呢?
答案肯定是不会啦,而且事实上KR算法效率很高
第一:不同子串hash值相同是小概率事件
第二:hash函数设计合理的情况下,计算速度相当快
第三:虽然理论上KR算法的时间复杂度是O(m*n),但现实应用中一般是O(m+n)
本文使用的hash函数如下:
hash(str[0..m-1])=(str[0]*2^(m-1)+str[1]*2^(m-2)+……+str[m-1]*2^0)mod q
q是一个较大的数,而且最好是素数,而且是大于m的素数。
计算时,*2的运算就使用移位代替了
在下面的实现代码中,q取整形最大值,也就是说,可以不用进行模运算了,这种偷工减料的做法只能在模式串很短的情形下才可以用,要不然会溢出的。粗略算了一下,如果模式串是ascii 英文字符,那么模式串长度不超过25的情况下,在32 位机上是没问题的
举例说一下KR算法吧:
在我写的这个总结中,各个算法使用的例子都一样,随便找的;
模式串 pattern="pappar"
文本串 text="pappappapparrassanuaragh";
pattern 长度记 pattern_len
预备阶段就是计算pattern的hash,长度为6,那么hash_pattern='p'*2^5+'a'*2^4+'p'*2^3+'p'*2^2+'a'*2^1+'r'*2^0
当然,这里使用的是字符的ascii值
也计算text前六个字符的hash,我们记第一个为hash_text[0]
然后就开始向前移动了,在移动时,要重新计算当前与模式串对应的串的hash值,这个工作叫rehash
初始化 i=0
如果 hash_pattern与hash_text[i]相等,返回 i
如果不等 计算新的hash值,就是text[i..i+patten_len]的hash,
当然这里不会像第一次那样全部计算,方法是
上一次计算的值减去上一次匹配时串的第一个字符乘以 2^pattern_len ,然后乘以2,再加上新加入比较的字符值
根据公式可以很清晰看出来。
就是减去计算中的第一项,把剩下的乘以2,然后在末尾加入新增的字符值
看代码吧,很简答的
- //Karp-Rabin algorithm,a simple edition
- int karp_rabin_search(const char* text,const int text_len,const char* pattern,const int pattern_len)
- {
- int hash_text=0;
- int hash_pattern=0;
- int i;
- //rehash constant:2^(pattern_len-1)
- int hash_const=1;
- /*for (i=1;i<pattern_len;i++){
- hash_const<<=1;
- }*/
- hash_const<<=pattern_len-1;
- //preprocessing
- //hashing
- for (i=0;i<pattern_len;++i){
- hash_pattern=(hash_pattern<<1)+pattern[i];
- hash_text=(hash_text<<1)+text[i];
- }
- //searching
- for (i=0;i<=text_len-pattern_len;++i){
- if (hash_pattern==hash_text&&memcmp(text+i,pattern,pattern_len)==0){
- return i;
- }else{
- //rehash
- hash_text=((hash_text-text[i]*hash_const)<<1)+text[i+pattern_len];
- }
- }
- return -1;
- }
hash函数的好坏会直接影响算法的效率,一般应遵循这样的规则:
1 要容易计算,本文中用的就不错,移位的速度大家是知道的
而且在rehash,就是重新计算hash值时,hash的构造要能避免重新计算整个串的hash,而应该像本例中用到的那样,可以动态地很容易地更新
2 字符串hash值要尽量分布均匀,减少冲突,这是hash函数在任何场合的要求。做到这一点,就能减少匹配中字符的一个个比较,提高性能。如果能够保证每个串的hash值不同,就不用再比较字符了,可以省掉代码中的memcmp运算
Monte Carlo改进的 RK算法就是只比较hash值,虽然那个改进的算法不能保证正确的结果,但以低于2.53/pattern_len的错误率,而很实用
转自 http://blog.youkuaiyun.com/touzani/archive/2007/05/30/1632149.aspx
字符串匹配(String matching)
算法
|
预处理时间
|
匹配时间
|
| ||
朴素算法
|
0
|
O((
n -
m + 1)
m)
|
Rabin-Karp
|
Θ(
m)
|
O((
n -
m + 1)
m)
|
有限自动机算法
|
O(
m |Σ|)
|
Θ(
n)
|
KMP算法
|
Θ(
m)
|
Θ(
n)
|
如果某个字符串 y ∈ Σ*,使得x=wy 。则称w是x的前缀, 记为w � x 。 如果w是x的后缀,记为w � x
可以把字符串匹配问题描述为 找出0 ≤ s ≤ n-m 并满足P � Ts+m的所有位移s
4 do if P[1 ‥ m] = T[s + 1 ‥ s + m] // 隐含着一个循环
5 then print "Pattern occurs with shift" s
因此,字符串"31415" 对应于十进制数31415
已知模式P[1..m],设p表示其相应十进制数地值,类似地, 对于给定的文本T[1..n]. 用
可以用霍纳规则(Horner’s rule) 在Θ(m) 的时间内计算p的值
p = P[m] + 10 (P[m - 1] + 10(P[m - 2] + · · · + 10(P[2] + 10P[1]) )).
如果能在总共Θ(n - m + 1) 时间内计算出所有的ts 的值,那么通过把p值与每个ts(有n-m+1个)进行比较,就能够在Θ(m) + Θ(n - m + 1)= Θ(n) 时间内求出所有有效位移。(计算出1个ts 就跟p比较,处理结果。)
为了在Θ(n - m) 时间内计算出剩余的值t1, t2, . . . , tn-m 可以在常数的时间内根据ts计算出ts+1,先看例子,假如m = 5,ts = 31415, 我们去掉高位数字T [s + 1] = 3,然后在加入一个低位数字T [s + 5 + 1](假设为2),得到:
ts+1 = 10(31415 - 10000 • 3) + 2 = 14152.
总结出公式: ——公式1
因此,可以在Θ(m)时间内计算出p和t0。然后在Θ(n - m + 1)时间内计算出t1, . . . , tn-m 并完成匹配。
现在来解决唯一的问题,就是计算中p和ts的值可能太大,超出计算机字长,不能方便地进行处理。如果p包含m个字符,那么, 关于在p上地每次算术运算需要“常数”时间这一假设就不合理了,幸运的是,对这一问题存在一个简单的补救方法,对一个合适的模q来计算p和ts的模,每个字符是一个十进制数,因为p和t0 以及公式1计算过程都可以对模q进行,所以可以在Θ(m)时间内计算出模q的p值,在Θ(n - m + 1)时间内计算出模q的所有ts值,通常选模q为一个素数,使得10q正好为一个计算机字长,单精度算术运算就可以执行所有必要的运算过程。 一般情况下,采用d进制的字母表{0, 1, . . . , d - 1}, 所选的q要满足d*q < 字长,调整公式1, 使其为:
但是加入模q后,由ts ≡ p (mod q)不能说明 ts = p. 但ts � p (mod q), 可以说明 ts ≠ p,
RABIN-KARP-MATCHER(T, P, d, q)
1 n ← length[T]
2 m ← length[P]
3 h ← dm-1 mod q
4 p ← 0
5 t0 ← 0
6 for i ← 1 to m � Preprocessing.
7 do p ← (dp + P[i]) mod q
8 t0 ← (dt0 + T[i]) mod q
9 for s ← 0 to n - m � Matching.
10 do if p = ts
11 then if P[1 ‥ m] = T [s + 1 ‥ s + m]
12 then print "Pattern occurs with shift" s
13 if s < n - m
14 then ts+1 ← (d(ts - T[s + 1]h) + T[s + m + 1]) mod q
void RABIN_KARP_MATCHER(string T, string P, int d, int q)
/*
搜索P在T中出现的位置
参数d :字母表的进制,亦即是字母表的元素个数
参数q : 一个较大的素数, 只需d*q < 字长
*/
{
int n= T.length();
int m= P.length();
if( n < m)
return ;
int i, h=1;
for(i=1; i<=m-1; i++) // caculate h
h = h*d%q;
int p=0, t=0;
for(i=0; i<m; i++) // 预处理,计算p, t0
{
p = (( d*p + P[i]) % q);
t = (( d*t + T[i]) % q);
}
int s;
for(s=0; s < n-m+1; s++) // 匹配
{
if( p == t )
{
for(i=0; i<m; i++) // 进一步验证
if(P[i]!=T[s+i])
break;
if(i==m)
cout<<"Pattern ocurs with shift "<<s<<endl;
}
if( s < n-m )
t= ( d* (t - T[s]*h%q + q) + T[s+m]) % q; // 计算ts+1
}
cout<<"string matching ends"<<endl;
return ;
}
实际应用中,有效位移数很少(常数c个),因此期望的匹配时间为O((n - m + 1) + cm) = O(n+m), 选取的素数q比p的长度m大得多。
通常处理ASCII码字符, d=128, 素数q可选6999997。
//*****************************************************************************************************************************
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define MAXN 100
const int d=128;
const int q=6999997;
int RK(char *T,char *pat)
{
int n=strlen(T),m=strlen(pat);
if(n<m) return -2;
int i,s,t=0,p=0,h=1;
for(i=1; i<m; i++) h=(h*d) % q; //h=d^m-1
for(i=0,p,t; i<m; i++)
{
p = ( d*p+pat[i] ) % q; //p[m]
t = ( d*t+T[i] ) % q; //t[m]
}
for(s=0; s<=n-m; s++)
{
if(p==t)
{
for(i=0; i<m; i++)
if(pat[i] != T[s+i]) break;
if(i==m) return s;
}
if(s<n-m)
t = ( d*(t-h*T[s]) + T[s+m] ) % q; //t[s+1]
}
return -1;
}
char s1[MAXN],s2[MAXN];
int main()
{
while(scanf("%s%s",s1,s2))
{
printf("%d\n",RK(s1,s2));
}
return 0;
}