这一节介绍一下由Rabin和Karp提出的RK算法。
1,RK算法的基本思想
HASH!
如果两个字符串hash后的值不相同,则它们肯定不相同;如果它们hash后的值相同,它们不一定相同。
RK算法的基本思想就是:将模式串P的hash值跟主串S中的每一个长度为|P|的子串的hash值比较。如果不同,则它们肯定不相等;如果相同,则再诸位比较之。
2,RK算法的求解过程
将我们用来比较的字符串的全集设为∑={a,b,…,z},设∑的长度为d=|∑|,则主串和模式串都可以看作是d进制数。例如只由数字组成的字符串,它的全集∑={0,1,2,3,4,5,6,7,8,9},d=10。
设模式串为P,其长度为m,主串为S,其长度为n。则模式串P可以看作是一个m位的d进制数A,主串S可以看作是一个n位的d进制数。我们的模式匹配过程就是将A与主串中的每个长度为m的d进制数S[t…t+m-1] (t=0,1,2,…,n-m+1)的值做比较,所以整个模式匹配过程就变成了两个d进制数之间的比较过程。例如模式串为123,主串为65127451234,就是将十进制数123跟十进制数651, 512, 127, 274, 745, 451, 512, 123的逐个比较过程。
明确了匹配过程,下面就是求解A和求解S[t…t+m-1] (t=0,1,2,…,n-m+1)的过程:
1)求解A。根据多项式计算方法,A = P[m-1] + d * (P[m-2] + d * (P[m-3] + …+ d * (P[1] + d*P[0])…))
2)求解S[t…t+m-1]。为了方便表示,我们设S[t…t+m-1] = St,则S[t+1…t+m] = St+1
假设已求得St,现在要求St+1,需要注意的是St+1是St去掉高位数据,其余的m-1位乘以d后再在最低位加一位得到。于是
St+1 = d * (St – dm-1*S[t]) + S[t+m]
公式比较晦涩,举个例子看看吧。比如上面例子中主串是65127451234。S2=127,那么S3=10×(127-102×1)+ 4 = 274
现在的问题是,如果A的值太大,比较的过程会比较耗时,这个时候我们可以将这个大数mod q(q是一个大素数),同理,st也mod q,将两个取模之后的数相比较。
3,RK算法的实现
1 #define q 144451
2 #define d 26
3
4 int isMatch(char *S, int i, char *P, int m) 5 { 6 int is, ip; 7 for(is=i, ip=0; is != m && ip != m; is++, ip++) 8 if(S[is] != P[ip]) 9 return 0; 10 return 1; 11 } 12
13 /*
14 * 字符串匹配的RK算法 15 * Author:Rabin & Karp 16 * 实现:CobbLiu 17 * 若成功匹配返回主串中的偏移,否则返回-1 18 */
19 int RK(char *S, char *P) 20 { 21 int m = strlen(P); 22 int n = strlen(S); 23 unsigned int h = 1
24 unsigned int A = 0; 25 unsigned int St = 0; 26 int i; 27
28 //初始化,算出最d进制下的最高位
29 for(i = 0;i < m - 1;i++) 30 h = (h*d) % q; 31
32 for(i = 0; i != m; i++){ 33 A = (d*A + (P[i] - 'a')) % q; 34 St = (d*St + (S[i] - 'a')) % q; 35 } 36
37 for(i = 0; i != n-m; i++){ 38 if(A == St) 39 if(isMatch(S,i,P,m)) 40 return i; 41 St = (d*(St - h*(S[i]-'a'))+(S[i+m]-'a')) % q; 42 } 43
44 return -1; 45 }
4,算法的复杂度分析
如果选择的素数q>=m, 则RK算法的期望运行时间为O(n+m), 如果m<<n,则算法的期望运行时间为O(n)。具体推理过程请参看《算法导论》第32章P562页。
从这篇文章起陆续学习一些模式匹配算法,首先是由大神高纳德Knuth其学生morris, pratt提出的KMP算法。
1,KMP的基本思想
KMP算法是本科数据结构课上面讲过的字符串匹配算法,它的基本思想是:
当模式串与主串出现不匹配时,主串指针不回溯,将模式串向右滑动尽可能远的距离与主串中当前位置处的字符再次进行匹配。
2,KMP算法描述
设我们的主串为s1s2...sn,模式串为p1p2...pm。
当前正在比较的是主串中的字符si与模式串中的pj,当匹配失效后,假设主串的si应该与模式串中的pk比较,则此时有以下的关系式:
p1p2…pk-1 = si-k+1si-k+2…si-1 (1)
根据已经得到的匹配结果,有以下的关系式:
pj-k+1pj-k+2…pj-1 = si-k+1si-k+2…si-1 (2)
根据(1)和(2)可以得出:
p1p2…pk-1 = pj-k+1pj-k+2…pj-1 (3)
根据上面的式子(3),我们设有数组next,令next[j]=k,则next[j]表示当模式中的第j个字符与主串中的字符j失配时,在模式串中需要重新和主串中该字符进行比较的位置。由此可以得出next数组的定义:
next[j] = 0 (当j=1时)
next[j] = Max{k|1<k<j 且p1p2…pk-1 = pj-k+1pj-k+2…pj-1}
next[j] = 1 (其他情况)
根据上面的讨论,next数组只跟模式串有关系,它可以根据上面的next定义递归求得,求解过程如下:
1) next[1]=0
2) 当j>1时,设next[j]=k, 则存在关系: p1p2…pk-1 = pj-k+1pj-k+2…pj-1
a,如果pk=pj,则有 p1p2…pk-1pk = pj-k+1pj-k+2…pj-1pj 于是 next[j+1]=next[j]+1
b,如果pk!=pj,此时可以理解为模式串在匹配自己的第j个字符时失配,要将第j个字符跟第k个字符匹配:
b1,如果pk=pnext[k],则说明在主串中的第j+1个字符前存在一个长度为next[k]的最长子串,和模式串中从首字符起长度为next[k]的子串相等,即:p1p2…pnext[k]-1= pj-next[k]+1pj-next[k]+2…pj-1,这就是说 next[j+1]=next[k]+1
b2,如果pk!=pnext[k],则继续按照b1的思路向前寻找,直到模式pj和某个字符匹配成功。若不成功,则next[j+1]=1
3,KMP的实现
首先是构造数组next的例程:
1 void get_next(char *T, int lenT, int next[]) 2 { 3 int i = 0, j = 0; 4 next[0] = 0; 5 while(i < lenT) 6 if(j == 0 || T[i] == T[j-1]) 7 { 8 ++i; 9 ++j; 10 next[i] = j; 11 } 12 else 13 j = next[j-1]; 14 }
然后是KMP算法主求解过程:
1 int Index_KMP(char *S, int lenS, 2 char T*, int lenT, 3 int next[]) 4 { 5 int i = 0, j = 0; 6 7 while(i < lenS && j < lenT) 8 if(j == 0 || S[i] == T[j]) 9 { 10 j++; 11 i++; 12 } 13 else 14 j = next[j-1]; 15 if(j >= lenT) 16 return i-lenT; 17 return -1; 18 }
4,KMP的时间复杂度
设主串的长度为n,模式串的长度为m,则构造next的时间复杂度为O(m),KMP算法的时间复杂度为O(n),所以KMP算法的时间复杂度为O(n).