前言
串的模式匹配
子串的定位操作通常称为模式匹配,它求的是子串(常称模式串)在主串中的位置。
前缀、后缀、部分匹配值
前缀:包含第一个字符且不包含最后一个字符的子串。
后缀:包含最后一个字符且不包含第一个字符的子串。
部分匹配值:字符串的最长相等的前后缀长度。
以"ababa"为例:
- "a"的前后缀均为空,最长相等的前后缀长度为0。
- "ab"的前缀为{"a"},后缀为{"b"},最长相等前后缀长度为0。
- "aba"的前缀为{"a","ab"},后缀为{"a","ba"},最长相等前后缀长度为1。
- "abab"的前缀为{"a","ab","aba"},后缀为{"b","ab","bab"},……2。
- "ababa"的前缀为{"a","ab","aba","abab"},后缀为{"a","ba","aba","baba"},……3。
故字符串"ababa"的部分匹配值为00123。
KMP算法
以主串为"ababcabcacbab",子串(模式串)为"abcac"为例。
求出子串的部分匹配值为00010,得到部分匹配值表(PM表):
a | b | c | a | c |
0 | 0 | 0 | 1 | 0 |
在匹配过程中,第一趟:
主串:a b a b c a b c a c b a b
子串:a b c
a与c不匹配,则查PM表得最后一个匹配字符b的匹配值为0,则子串向后移动的位数:
移动的位数=已匹配的字符数-对应部分的匹配值
则子串向后移动2-0=2位,继续匹配,如此往复直到主串或子串走完。
上述每遇到不匹配的字符,都去找前一个字符的部分匹配值,所以干脆优化一下PM表,将表中元素统一向右移一位,得next数组:
a | b | c | a | c |
-1 | 0 | 0 | 0 | 1 |
- 第一个元素右移后的空缺用-1来填补,因为若是第一个字符不匹配,则子串直接右移一位,用上述公式计算右移的位数也是0-(-1)=1,正确。
- 由于最后一个元素的部分匹配值是给下一个元素用的,而没有下一个元素,所以可以舍去。
设编号为j(从1开始),则后移位数Move=(j-1)-next[j],由于在实际匹配过程中子串在内存里是不会移动的,只是子串的指针在移动。子串在主串上向后移动Move位,相当于子串的指针向前移动Move位。所以相当于将子串的指针j回退到 j=j-Move=next[j]+1。
所以可以将next数组优化一下,所有元素+1:
编号j | 1 | 2 | 3 | 4 | 5 |
子串S | a | b | c | a | c |
next | 0 | 1 | 1 | 1 | 2 |
综上所述,next数组是在PM表基础上元素整体右移后+1得到的。
next[j]的含义是:在子串的第j个字符与主串发生失配时,则跳到子串的next[j]位置重新与主串当前位置进行比较。
KMP算法的时间复杂度为O(m+n)(O(m)求出next数组,O(n)遍历主串),其优点是主串不回溯。
改进的KMP算法
next数组还可以进一步优化,以主串"aaabaaaaab",子串"aaaab"为例:
主串:a a a b a a a a a b
子串:a a a a b
第一趟匹配a与b不匹配,如果按照next数组,子串指针j又得移到子串第一个字符开始与b匹配,实际大可不必,前面都是a肯定与b不匹配。
当子串第j个元素与主串失配,j需要跳回next[j]处,如果next[j]处的元素与j处的元素相等,那么next[j]处的元素必然失配,故需要接着往前找next[next[j]],如果next[next[j]]处的值不等于j处的值,则将next[j]修正为next[next[j]],如果二者相等,则继续向前寻找next,直至两者不相等为止,更新后的数组命名为nextval。
如下图所示:
模式串S | a | a | a | a | b |
j | 1 | 2 | 3 | 4 | 5 |
next[j] | 0 | 1 | 2 | 3 | 4 |
nextval[j] | 0 | 0 | 0 | 0 | 4 |
总结
求next数组
- 依次求得各子串的部分匹配值,生成PM表。
- PM表元素统一右移一位,首位用-1填充。
- 所有元素+1。
求nextval数组
比较每个元素与其next处的元素,若相等,则接着往前找next的next,直到找到与当前元素不同的元素的编号,将该编号赋给先前遍历过的next值。
目的就是为了省去相同的不匹配的字符重复比较的次数。
C语言实现
//KMP算法(包括改进KMP算法)
#define MAXLEN 25
#include<stdio.h>
#include<string.h>
typedef struct{
char ch[MAXLEN];//ch[0]不用,从ch[1]开始存,位序与下标相等,能存24个数据元素
int length;
}SString;
void get_next(SString T,int next[]){//求next数组
int i=1,j=0;//next[i]=j
next[1]=0;//特别地,next[1]=0
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
i++;
j++;
next[i]=j;
}else{
j=next[j];
}
}
}
void get_nextval(SString T,int nextval){
int i=1;j=0;//nextval[i]=j
nextval[1]=0;
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
i++;
j++;
if(T.ch[i]!=T.ch[j]) nextval[i]=j;
else nextval[i]=nextval[j];//消除递归,追根溯源
}
else{
j=nextval[j];
}
}
}
int index_KMP(SString S,SString T,int next[]){//KMP
int i=1,j=1;//主串和模式串的指针
while(i<=S.length&&j<=T.length){
if(j==0||S.ch[i]==T.ch[j]){//当第一个字符就不匹配时,ij都右移一位
i++;
j++;
}else{
j=next[j];
}
}
if(j>T.length)return i-T.length;//匹配成功
return 0;//匹配失败
}
int main(){
char* ss="googlegoogle";
char* tt="abaabcaba";
SString S,T;
//init
for(int i=1;i<13;i++){
S.ch[i]=ss[i-1];
}
S.length=12;
for(int i=1;i<10;i++){
T.ch[i]=tt[i-1];
}
T.length=9;
int next[9];
get_next(T,next);
for(int i=1;i<=9;i++)
printf("%d ",next[i]);
return 0;
}