字符串匹配算法 -- Rabin-Karp 算法
参考资料
1 算法导论
2 lalor
3 记忆碎片
Rabin-karp 算法简介
在实际应用中,Rabin-Karp 算法对字符串匹配问题能较好的运行。Rabin-Karp 算法需要对字符串和模式进行预处理,其预处理时间为 O ( m ) ,在最坏情况下的运行时间为 O ( ( n-m+1 ) m ) ,但基于某种假设(不知道是何种假设),它的平均情况下的运行时间还是比较好的。
为了便于说明,假设
∑ = { 0,1,2.....9 },这样每个字符都是一个十进制数字。(对于更一般的情况,可以假设每个字符都是基数为 d 的表示法中的一个数字,d = | ∑ | 。)可以用一个长度为 k 的十进制数来表示由 k 个连续字符组成的字符串。因此,字符串31415 就对应于十进制数 31415 。
∑ = { 0,1,2.....9 },这样每个字符都是一个十进制数字。(对于更一般的情况,可以假设每个字符都是基数为 d 的表示法中的一个数字,d = | ∑ | 。)可以用一个长度为 k 的十进制数来表示由 k 个连续字符组成的字符串。因此,字符串31415 就对应于十进制数 31415 。
已知一个模式 P[ 1.. m ],设 p 表示该模式所对应的十进制数的值(如模式 P = "31415" ,数值p = 31415)。对于给定的文本 T [ 1.. n ],用 ts 来表示其长度为 m 的子字符串 T [ s+1.. s+m ] (s = 0,1,.. n-m)相对应的十进制数的值。ts = p 当且仅当 T [ s+1.. s+m ] = P[ 1.. m ] ,因此 s 是有效位移当且仅当 ts = p 。
预处理 -- p 和 t0
于是应用霍纳法则(Horner's Rule)在 O ( m ) 的时间内计算 p 的值:
p = P[ m ] + 10( P[ m-1 ] + 10 ( P[ m-2 ] + .. + 10( P[ 2] + P[ 1 ]) ... ))
类似的,也可以在 O ( m ) 时间内根据 T[ 1.. m ] 计算出 t0 的值。
为了在 O ( n - m ) 的时间内计算出剩余的值 t1,t2,...,t ( n - m ),可以在常数时间内根据 ts 计算出 t ( s+1 )。因为
t ( s + 1) = 10 ( ts - 10^(m-1) T [ s+1 ] ) + T [ s + m +1] ( 1 )
事实上,就是去掉最高位,然后左移了一位,在加上 T [ s + m +1] ,就得到了 t ( s + 1) 。
预处理的时间为 O ( m )
字符串匹配
当进行完预处理之后,就可以执行字符串匹配了。我们只需要将 ti ( i = 0 , 1 , ... n-m ) 与 p 进行比较,相等则为合法匹配,否则为非法匹配。整个匹配过程的时间为 O ( n -m + 1 )
然而,上述问题对于模式 p 的长度较小时,比较方便。当 p 和 ts 的值很大时,p 的结果会太大,以至于不能很好的处理这类问题 。所以才有了下面的改进版本。
补救方法
对一个合适的模 q 来计算 p 和 ts 的模。每个字符是一个十进制数,因为 p ,t0 以及递归式 1 计算过程都可以对模 q 进行,所以可以在 O ( m ) 时间内计算出模 q 的 p 值。在时间 O( n-m+1 ) 计算出模 q 的所有 ts 值。通常选模 q 为一个素数,使得 10q 正好为一个计算机字长。
在一般情况下,采用 d 进制的字母表 { 0 ,1,... ,d - 1 } 时,所选取的 q 要满足使 dq 的值在一个计算机字长内,并调整递归式 ( 1 ) 以使对模 q 进行运算,使其成为
t ( s + 1) = ( d ( ts - T [ s+1 ] h ) + T [ s + m +1] ) mod q
其中 h
≡ d ^ ( m-1 ) (mod q) 。
≡ d ^ ( m-1 ) (mod q) 。
加入模 q 后,我们已经不能通过 ts ≡ p (mod q ) 并不能说明 ts = p 。当 ts ≡ p (mod q ) 不成立时,则肯定 ts != p 。因此,当 ts ≡ p (mod q ) 时我们还需要进一步进行测试,看看 ts 是否等于 p ,因为 ts 可能是匹配的也有可能是伪匹配的。
这个算法就是有点使用hash的思想了。把模式字符串进行一个预处理,并mod,主字符串进行逐个进行简单的hash映射,然后mod比较。
伪代码如下
RABIN-KARP-MATCHER( T,P,d,q)
1 n ← length[ T ]
2 m ← length[ P]
3 h ← d^(m-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 s-m Matching( 匹配 )
10 do if p = t
11 then if P[1..m] = T[s+1..s+m] 对p 和 T 中的每个字符进行判断
12 then print "匹配"
13 if s < n - m
14 then t(s+1) ← (d (ts - T[s+1] h) + T[s+m+1]) mod q
代码实现
*Copyright(c) Computer Science Department of XiaMen University
*
*Authored by laimingxing on: 2012年 03月 04日 星期日 18:18:28 CST
*
* @desc:
*
* @history
*/
// d = 256 ; q = 127 void RABIN_KARP_MATCHER( char *T, char *P, int q)
{
assert( T && P && q > 0 );
int M = strlen( P );
int N = strlen( T );
int i, j;
int p = 0;//hash value for pattern
int t = 0;//hash value for txt
int h = 1; //the value of h would be "pow( d, M - 1 ) % q "
for( i = 0; i < M - 1; i++)
h = ( h * d ) % q; for( i = 0; i < M; i++ )
{
p = ( d * p + P[i] ) % q;
t = ( d * t + T[i] ) % q;
} //Slide the pattern over text one by one
for( i = 0; i <= N - M; i++)
{
if( p == t)
{
for( j = 0; j < M; j++)
if(T[i+j] != P[j])
break;
if( j == M )
printf("Pattern occurs with shifts: %d\n", i);
}
//Caluate hash value for next window of test:Remove leading digit,
//add trailling digit
if( i < N - M )
{
t = ( d * ( t - T[i] * h ) + T[i + M] ) % q;
if( t < 0 )
t += q;//按照书上的伪代码会出现t为负的情况,则之后的计算就失败了。
}
}
}
Rabin-Karp-Matcher 的预处理时间为 O ( m ) ,其匹配时间在最坏情况下为 O ( ( n- m + 1) m) ,
虽然 Rabin-Karp-Matcher 在最坏的情况下与朴素匹配一样,但是实际应用中往往比朴素算法快很多,应用还是很广的。