书中依次讲了4种方法,朴素算法、RabinKarp算法、有限自动机算法、KMP算法
1、朴素算法:
算法用一个循环来找出所有有效位移,该循环对n-m+1个可能的每一个s值检查条件P[1...m] = T[s+1....s+m];
//朴素的字符串匹配算法 void nativeStringMatcher(string st, string sp) { int n = st.length(); //主串长度 int m = sp.length(); //模式串长度 for (int s = 0; s <= n-m+1; s++) { string ss = st.substr(s, m); //ss为st从s位置开始的m个字符,即和sp比较的字符串 if (sp == ss) cout << "在位置" << s << "处发现匹配" << endl; } }
上面函数调用了string比较是否相等的操作,如果不用这样比较,即一个字符一个字符比较,则有暴力枚举法如下:
int ViolentMatch(char* s, char* p) { int sLen = strlen(s); int pLen = strlen(p); int i = 0; int j = 0; while (i < sLen && j < pLen) { if (s[i] == p[j]) { //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++ i++; j++; } else { //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0 即j回到0,i回到i-j+1处重新比较,下面介绍的KMP算法的不同就在于i不会回到i-j+1,而j也不一定非要回到0比较 i = i - j + 1; j = 0; } } //匹配成功,返回模式串p在文本串s中的位置,否则返回-1 if (j == pLen) return i - j; else return -1; }
运行时间为O(n),预处理时间为O(m)
举个例子,如果给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,现在要拿模式串P去跟文本串S匹配,过程如下所示:
此时i 不是回到i-j+1 (而是不动或者+1),j 也不是回到0,下面主要的问题是求 j 回到那里。
//对于P的前j+1个序列字符(下面的k=next[j],即前j个字符的前缀串长度,下标从0到j-1): // // 1、若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;//前缀串增加1,比如ABABC...ABABC,k=4,p[j]=p[k]=p[4]=C // 2、若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] = next[k] + 1; //比如ABABC....ABABA,此时之前k=4(ABAB),因为p[4]!=p[j],next[k]=next[4]=2(ABAB);然后p[2]=p[j]=A,所以next[j+1]=next[k]+1=3(前缀串为ABA) //否则继续递归前缀索引k = next[k],而后重复此过程,直到k=-1,那么next[j+1]=0。 void computePrefixFunction(char* p,int *next) { int pLen = strlen(p); next[0] = -1; //第一个字符的前缀串长度设为-1,第2个为0 int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 //如果k=-1,则next[j]=0,即前j-1个字符没有前缀,或者如果k!= -1且p[j] == p[k],则前j-1个字符的next=k + 1(k先自增然后赋给next),然后求下一个字符的next if (k == -1 || p[j] == p[k]) { ++k; ++j; next[j] = k; } else //如果k != -1,则把next[k]赋给k,继续找当前字符j之前的前缀串长度 { k = next[k]; } } }结果如图所示:
KMP算法:
int KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); int *next = (int *)malloc(sizeof(int) * pLen);//存储p中第i个字符前的字符串的前缀串长度(比如ABCABD,当前字符为D,下标为5,则p[5]=2,"AB"="AB") computePrefixFunction(p,next); while (i < sLen && j < pLen) { ////①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++ ////j=-1说明j = next[j],即当前字符j没有前缀串,故j要重头开始重新查找 //if (j == -1 || s[i] == p[j]) //{ //i++; //j++; //如果j = -1 ,则此时j = 0 ,即j又从头开始判断 //} //else //{ ////②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j] ////next[j]即为j所对应的next值 //j = next[j]; //j调回到最长前缀的下一个字符,比如ABC....ABD,因为next[D]=2,则j调回到C,C再重新和i判断。比如ABC...MND,则next[D]=-1,则j=-1,下次判断后j回到0,重新判断 //} //如果当前字符相等,则i++,j++ if(s[i] == p[j]) { i++; j++; } else //否则,j退回到next[j] { j=next[j]; //如果退回后的j为-1,说明退到了p[0],因为只有p[0]=-1,则令j=0,重头判断,并且i++,比如s="ABC",p="DEF",'A'!='D'则重新判断'B'和'D' //注意如果退回后的j=0,则j也退到了p[0],但是这时i不能++,因为s[i]有可能=p[0],比如说s="...ABCABCD",P="ABCD",当判断到A!=D时,j退回到了0,这是还要继续判断s[i]是否='A‘ if(j == -1 )//j=-1,说明上个j为第1个字符,即p[0],即[0]!=p[i],那么令j=0,i则向后挪一位 { j=0; i++; } } } if (j == pLen) return i - j; else return -1; }
#include <iostream> using namespace std; int ViolentMatch(char* s, char* p) { int sLen = strlen(s); int pLen = strlen(p); int i = 0; int j = 0; while (i < sLen && j < pLen) { if (s[i] == p[j]) { //①如果当前字符匹配成功(即S[i] == P[j]),则i++,j++ i++; j++; } else { //②如果失配(即S[i]! = P[j]),令i = i - (j - 1),j = 0 i = i - j + 1; j = 0; } } //匹配成功,返回模式串p在文本串s中的位置,否则返回-1 if (j == pLen) return i - j; else return -1; } //对于P的前j+1个序列字符(下面的k=next[j],即前j个字符的前缀串长度,下标从0到j-1): // // 1、若p[k] == p[j],则next[j + 1 ] = next [j] + 1 = k + 1;//前缀串增加1,比如ABABC...ABABC,k=4,p[j]=p[k]=p[4]=C // 2、若p[k ] ≠ p[j],如果此时p[ next[k] ] == p[j ],则next[ j + 1 ] = next[k] + 1; //比如ABABC....ABABA,此时之前k=4(ABAB),因为p[4]!=p[j],next[k]=next[4]=2(ABAB);然后p[2]=p[j]=A,所以next[j+1]=next[k]+1=3(前缀串为ABA) //否则继续递归前缀索引k = next[k],而后重复此过程,直到k=-1,那么next[j+1]=0。 void computePrefixFunction(char* p,int *next) { int pLen = strlen(p); next[0] = -1; //第一个字符的前缀串长度设为-1,第2个为0 int k = -1; int j = 0; while (j < pLen - 1) { //p[k]表示前缀,p[j]表示后缀 //如果k=-1,则next[j]=0,即前j-1个字符没有前缀,或者如果k!= -1且p[j] == p[k],则前j-1个字符的next=k + 1(k先自增然后赋给next),然后求下一个字符的next if (k == -1 || p[j] == p[k]) { ++k; ++j; next[j] = k; } else //如果k != -1,则把next[k]赋给k,继续找当前字符j之前的前缀串长度 { k = next[k]; } } } int KmpSearch(char* s, char* p) { int i = 0; int j = 0; int sLen = strlen(s); int pLen = strlen(p); int *next = (int *)malloc(sizeof(int) * pLen);//存储p中第i个字符前的字符串的前缀串长度(比如ABCABD,当前字符为D,下标为5,则p[5]=2,"AB"="AB") computePrefixFunction(p,next); while (i < sLen && j < pLen) { ////这里是July原文:①如果j = -1,或者当前字符匹配成功(即S[i] == P[j]),都令i++,j++ ////j=-1说明j = next[j],即当前字符j没有前缀串,故j要重头开始重新查找 //if (j == -1 || s[i] == p[j]) //{ //i++; //j++; //如果j = -1 ,则此时j = 0 ,即j又从头开始判断 //} //else //{ ////②如果j != -1,且当前字符匹配失败(即S[i] != P[j]),则令 i 不变,j = next[j] ////next[j]即为j所对应的next值 //j = next[j]; //j调回到最长前缀的下一个字符,比如ABC....ABD,因为next[D]=2,则j调回到C,C再重新和i判断。比如ABC...MND,则next[D]=-1,则j=-1,下次判断后j回到0,重新判断 //} //如果当前字符相等,则i++,j++ (注:这里是我修改了一下) if(s[i] == p[j]) { i++; j++; } else //否则,j退回到next[j] { j=next[j]; //如果退回后的j为-1,说明之前比较的就是第一个字符,即next之前的j=0,因为只有next[0]=-1,则令j=0,从头判断,并且i++,比如s="ABC",p="DEF",'A'!='D'则重新判断'B'和'D' //注意如果退回后的j=0,则j也退到了p[0],但是这时i不能++,因为s[i]有可能=p[0],比如说s="...ABCABCD",P="ABCD",当判断到A!=D时,j退回到了0,这是还要继续判断s[i]是否='A‘ if(j == -1 )//j=-1,说明上个j为第1个字符,即p[0],即[0]!=p[i],那么令j=0,i则向后挪一位 { j=0; i++; } } } free(next); if (j == pLen) return i - j; else return -1; } int main() { char *s="xyzabcbadmnnext"; char *p="abcbad"; cout<<KmpSearch(s,p)<<endl; //cout<<ViolentMatch(s,p)<<endl; return 0; }