标签:
子串的定位操作通常称为串的模式匹配,是串中最重要的操作之一。
假设要从下面的主串S=”goodgoogle”中,找到T=”google”这个子串的位置,通常通过以下几个步骤:
简单来说,就是对主串的每个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,知道匹配成功或全部遍历完成为止。之前使用串其他操作实现模式匹配(Index方法),现不用串其他操作,使用基本的数组实现,假设主串S和要匹配的子串T的长度均保存在S[0]和T[0]中,具体代码如下:
/*返回子串T和主串S中第pos个字符之后的位置。若不存在,则函数返回值为0*/
/*T非空,1<=pos<=StrLength(S)*/
int Index(String S,String T,int pos){
int i=pos; //i-主串S中当前位置下标,若pos不为1,从pos位置开始匹配
int j=1; //j用于子串T中当前位置的下标值
while(i <= S[0] && j <= T[0]){ //i小于S长度且j小于T长度时循环
if(S[i]==S[j]){ //两字母相同时,继续循环,向后移动
++i;
++j;
}else{ //俩字母不同时,i退回到上次匹配首位的下一位,j重置为1
i=i-j+2;
j=1;
}
}
if(j>T[0]) return i-T[0]; //若j比T的长度大时,返回首位,否则返回0
else return 0;
}
小结:最好情况的时间复杂度为
O(n+m) ,最坏情况的时间复杂度为O[(n?m+1)?m] 。所以,这个算法太低效了。
对于所有在子串中有与首字符相等的字符,是可以省略一部分不必要的判断步骤。朴素的模式匹配中有很多重复的遍历步骤,主串的i是需要不断回溯来完成,而分析发现,这种回溯可以是不需要的,而KMP模式匹配算法就是避免这种不必要的回溯发生。
既然i值不回溯,也就是不能变小,要考虑的变化就是j值,观察发现,T串的首字符与自身后面字符比较,发现如果有相等字符,j值的变化会不同,即,j值的变化与主串没什么关系,关键取决于T串的结构是否有重复的问题。
j值得多少取决于当前字符之前的串的前后缀的相似度。
T串各位置的j值变化定义为一个数组next,那么next的长度就是T串的长度,函数定义如下:
next[j]=0 ,当j=1 时;
next[j] =Max {k|1<k<j,且′p1...p′k?1=′pj?k+1...p′j?1 } 当此集合不为空时;(有重复子串)
next[j]=1 ,其他情况
举一些具体的例子:
eg1:T=”abcdex“
j | 123456 |
---|---|
模式串T | abcdex |
next[j] | 011111 |
1)当j=1时,next[1]=0;
2)当j=2时,j由1到j-1就只有字符’a’,属于其他情况 next[2]=1;
3)当j=3时,j由1到j-1串是’ab’,显然’a’与’b’不相等,属于其他情况,next[3]=1;
4)以后同理,所以最终T串的next[j]为011111。
eg2:T=”abcabx“
j | 123456 |
---|---|
模式串T | abcabx |
next[j] | 011123 |
1)当j=1时,next[1]=0;当j=2时,同上例说明, next[2]=1;当j=3时,同上,next[3]=1;当j=4时,同上,next[4]=1;
2)当j=5时,此时j由1到j-1的串是’abca’,前缀字符’a’与后缀字符’a’相等,所以k值为2,因此next[5]=2;
3)当j=6时,此时j由1到j-1的串是’abcab’,前缀字符’ab’与后缀字符’ab’相等,所以next[6]=3。
eg3:T=”ababaaaba“
j | 123456789 |
---|---|
模式串T | ababaaaba |
next[j] | 011123 |
1)当j=1时,next[1]=0;当j=2时,同上next[2]=1;当j=3时,同上,next[3]=1;
2)当j=4时,j由1到j-1的串是’aba’,前缀字符’a’与后缀字符’a’相等,所以next[4]=2;
3)当j=5时,此时j由1到j-1的串是’abab’,前缀字符’ab’与后缀字符’ab’相等,所以k值为3,因此next[5]=3;
4)当j=6时,此时j由1到j-1的串是’ababa’,前缀字符’aba’与后缀字符’aba’相等,所以next[6]=4;
5)当j=7时,此时j由1到j-1的串是’ababaa’,前缀字符’a’与后缀字符’a’相等,所以next[7]=2;
6)当j=8时,此时j由1到j-1的串是’ababaaa’,前缀字符’a’与后缀字符’a’相等,所以next[8]=2;
7)当j=9时,此时j由1到j-1的串是’ababaaab’,前缀字符’ab’与后缀字符’ab’相等,所以next[9]=3。
具体代码如下:
/*通过计算返回子串T的next数组,计算出当前要匹配的串T的next数组*/
void get_next(String T,int *next){
int i=1,j=0;
next[1]=0;
while(i<T[0]){ /*此处T[0]表示串T的长度,遍历子串T*/
if(j==0 ||T[i]==T[j]){
/*T[i]表示后缀的单个字符;T[j]表示前缀的单个字符*/
++i;++j;next[i]=j;
}else j=next[j]; //若字符不相同,则j值回溯
}
}
/*返回子串T在主串S中第pos个字符之后的位置。若不存在,则函数返回值为0*/
/*T非空,1<=pos<=StrLength(S)*/
int Index_KMP(String S,String T,int pos){
int i=pos; //i用于指示主串S当前位置下标,若pos不为1,从pos位置开始
int j=1; //用于指示子串T中当前位置下标
int next[255]; //定义next数组
get_next(T,next); //对串T进行分析,得到next数组
while(i<=S[0] && j<=T[0]){ //i小于S长度,j小于T长度
if(j==0 || S[i]==T[j]){
//相对于朴素算法增加j=0判断,俩字母相等继续
++i;++j;
}else{ //匹配失败,指针退回,重新匹配,i值不变,j值由next[j]确定
j=next[j]; //j退回合适位置,i值不变
}
}
if(j>T[0]) return j-T[0];
else return 0;
}
相比较于朴素匹配算法,改动不算大,关键在于去掉i值回溯部分。因为只涉及简单的单循环,时间复杂度为
O(m) ,由于i值不回溯,KMP效率得到提升,相比较朴素模式匹配是要好的。注意:KMP算法仅在模式与主串之间存在很多”部分匹配”的情况下才有优势,否则量算法差异不明显。
例如当主串S=”aaaabcde”,子串T=”aaaaax”,其next数组值分别为012345。开始时,当i=5,j=5时,b与a不相等,为步骤1;因此j=next[5]=4,i=5,为步骤2;此时b与a依旧不等,j=next[4]=3,为步骤3;此时b与a依旧不等,j=next[3]=2,为步骤4;此时b与a依旧不等,j=next[2]=1,为步骤5;此时b与a依旧不等,j=next[1]=0,根据算法,i++,j++,得到i=6,j=1,为步骤6。
分析发现,其中步骤2,3,4,5是多余的判断。由于T串的第二、三、四、五位的字符均和首位”a”相等,那么可以用首位next[1]来取代与它相等的字符后续next[j]的值,所以对next函数进行改进。
假设取代的数组为nextval,具体代码如下:
/*求模式串T的next函数修正值并存入数组nextval*/
void get_nextval(String T,int *nextval){
int i=1,j=0;
nextval[0]=1;
if(j==0 || T[i]==T[j]){
++i;++j;
if(T[i] !=T[j]){ //若当前字符与前缀字符不同
nextval[i]=j; //则当前的j为nextval在i位置的值
}else{
//若字符相同,则将前缀字符的nextval值赋值给nextval在i上的值
nextval[i]=nextval[j];
}
}
else j=nextval[j]; /*若字符不相同,则j值回溯*/
}
}
至于匹配算法,只要将“get_next(T,next)”改为“get_nextval(T,next)”即可,就不再重复讲述。
和next值求导一样,举几个具体的例子来说明,如下:
eg1:T=”ababaaaba”,
j | 123456789 |
---|---|
模式串 | ababaaaba |
next[j] | 011234223 |
nextval[j] | 010104210 |
1)当j=1时,nextval[1]=0;
2)当j=2时,因第二字符’b’的next值为1,第一位是’a’,l两个字符不相等,所以nextval[2]=next[2]=1;
3)当j=3时,因为第三字符’a’的next值为1,所以和第一位的’a’比较得知二者相等,所以,nextval[3]=nextval[1]=0;
4)当j=4时,第四字符’b’的next值为2,所以和第二位的’b’比较得知二者相等,所以,nextval[4]=nextval[2]=1;
5)当j=5时,第五字符’a’的next值为3,第五个字符’a’和第三位的’a’相等,所以,nextval[5]=nextval[3]=0;
6)当j=6时,第六字符’a’的next值为4,第六个字符’a’和第四位的’b’不相等,所以,nextval[6]=4;
7)当j=7时,第七字符’a’的next值为2,第七个字符’a’和第二位的’b’不相等,所以,nextval[7]=2;
8)当j=8时,第八字符’b’的next值为2,第八个字符’b’和第二位的’b’相等,所以,nextval[8]=nextval[2]=1;
9)当j=9时,第九字符’a’的next值为3,第九个字符’a’和第三位的’a’相等,所以,nextval[9]=nextval[3]=0。
eg2:T=”aaaaaaaab”,
j | 123456789 |
---|---|
模式串 | aaaaaaaab |
next[j] | 012345678 |
nextval[j] | 000000008 |
1)当j=1时,nextval[1]=0;
2)当j=2时,因第二字符’a’的next值为1,第一位是’a’,l两个字符相等,所以nextval[2]=next[1]=0;
3)同理,其后均为0 ……;
9)当j=9时,第九字符’b’的next值为8,第九个字符’b’和第八位的’a’不相等,所以,nextval[9]=nextl[9]=8。
小结:改进的KMP算法,在计算next值得同时,若a位字符与它next值指向的b位字符相等,则该a位的nextval就指向b位的nextval值,如果不等,则该a位的nextval值就是它自己a位的next值。
标签:
原文地址:http://blog.csdn.net/dengpei187/article/details/51883628