今天来记录一下,关于字符匹配的KMP算法。
1.引言
给定字符串text和pattern,需要判断字符串pattern是否为test的子串。pattern一般称为模式串,text为文本串。若匹配成功,则让函数返回,匹配开始处的下标,否则,返回-1。
2.KMP中的核心—next数组的求解
假设有一个字符串s,它以i号位作为结尾的子串就是s[1…i]。对该字串来说,长度为 k+1 的前缀和后缀分别是 s[0…k] 和 s[i - k…i] 。现在定义一个 next 数组,其中 next[i] 表示使子串 s[0…i] 的前缀 s[0…k] 等于后缀 s[i-k…i] 的最大的 k (注意:前缀后缀可以部分重合,但不能是 s[0…i] 本身),如果找不到相等的前后缀,就令next[i] = -1。显然 next[i] 就是所求最长相等前后缀中前缀的最后一位的下标。
//对于next数组而言,next[0]总是等于-1,因为对第一位来说,其不存在公共前后缀,而又为了其他位比较时,从上一位next[0]多一位的1开始比较。
void GetNext(char pattern[], int m){
next[0] = -1;
int j = -1;
for(int i = 1; i < m; ++i){ //对该字符串每一位进行next数组的求解
while(j != -1 && pattern[i] != pattern[j + 1]){ //j == -1是边界条件
j = next[j];
}
if(pattern[i] == pattern[j + 1]){
j++; //令next[i] = j + 1,令next指向该位置
}
next[i] = j; //每次让j记录上一字符的next值
}
return;
}
3.KMP算法
//对于该算法,只有pattern上的指向在不断由于i的更新改变
int KMP(char text[], char pattern[], int n, int m){ //n和m分别为text和pattern数组的长度
GetNext(pattern, m); //计算pattern的next数组
int j = -1; //初始化j为-1,代表当前没有任意一位被匹配
for(int i = 0; i < n; ++i){
while(j != -1 && text[i] != pattern[j + 1]){
j = next[j];
}
if(text[i] == pattern[j + 1]){
j++; //text[i]与pattern[j + 1]匹配成功,令j加一
}
if(j == m - 1){ //pattern完全匹配
return i - j;
}
}
return -1; //不匹配返回-1
}
上述代码与求解next的代码惊人的相似,其实求解next数组的过程就是模式串pattern进行自我匹配的过程。
计算mext数组的时间复杂度为O(m),匹配过程的时间复杂度为O(n),故KMP算法的时间复杂度为O(m + n)。
4.优化next数组
上述代码中关于匹配中回退,从匹配的角度看,每次匹配不成功,next[j]表示当模式串的 j + 1 位失配的时候,j应当退到的位置,仔细思考,每次我们退的时候,j会进行多次无意义的回退,即他总是回到 next[j] 的位置,但全然不管 pattern[j + 1] == pattern[next[j] + 1] 的情况,即这一次回退之后还是匹配不上。要让 next[j] 进行改变,一步回退到恰当的位置,就要对 next 数组进行变化。优化后 nextval 数组的含义为当模式串 pattern 的 i+1 位发生失配的时候,i 应当回退到的最佳位置。
void GetNextval(char pattern[], int m){
nextval[0] = -1;
for(int i = 1; i < m; ++i){ //对该字符串每一位进行next数组的求解
while(j != -1 && s[i] != s[j + 1]){ //j == -1是边界条件
j = nextval[j];
}
if(s[i] == s[j + 1]){
j++; //令next[i] = j + 1,令next指向该位置
}
if(j == -1 || s[i + 1] != s[j + 1]){
nextval[i] = j; //每次让j记录上一字符的next值
}
else{
nextval[i] = nextval[j];
}
}
return;
}
注:nextval只是跳过了无意义的匹配,并不会导致漏解,而由nextval的含义,GetNextval 和 KMP中的 while 都可以换成 if ,因为最多只会执行一次。