穷举的模式匹配算法时间代价:最坏情况比较n-m+1趟,每趟比较m次,总比较次数达(n-m+1)*m。复杂度为O(m*n)。原因在于每趟重新比较时,目标串的检测指针要回退,而这回退后再进行的计算很多是没必要。改进的模式匹配算法(KMP算法)可使目标串的检测指针不回退。总的比较次数最坏为n,求next函数的比较次数为m,所以总的时间复杂度为O(n+m)。
假设S串匹配到i位置,T串匹配到j位置,那么穷举法和KMP算法的区别就在于失配情况下所做的处理。我们的字符串下标从0开始。穷举法中,如果当前字符匹配,即S[i]=T[j],则令i++, j++,继续匹配下一个字符;如果失配,即S[i]!=T[j],即S[i-j]…S[i-1]=T[0]…T[j-1],所以此时的i= i-j+1, j=0。可以看出每次失配,T只相对向右移动一位,这样的效率很低,因为在T[0]…T[j-1]其实已经和S比较了,然而一旦失配后,T又需要重新从T[0]开始和S比较。
int index(char s[], char t[], int pos)
{
int lens = strlen(s);
int lent = strlen(t);
int i = pos;
int j = 0;
while(i < lens && j < lent){
if(s[i] == t[j]){
i ++;
j ++;
}
else{
i = i - j + 1;
j = 0;
}
}
if(j >= lent){
return i - lent; //输出的是目标串开始匹配字符的下标
}
else{
return -1;
}
}
在KMP算法中,如果当前字符匹配成功,即S[i]=T[j],和穷举法一样继续匹配下一个字符,即i++, j++;如果失配,即S[i]!=T[j],但同时暗含的是S[i-j]…S[i-1]=T[0]…T[j-1],看下面过程:
ababcabcaabcbaab
ababcabababc
S[i]!=T[j]时,右移T串。
ababcabcaabcbaab
ababcabababc
而绿色的不需要比较,是因为S[0]=T[0],S[0]!=S[1],显然S[1]!=T[0]。T串继续右移。
ababcabcaabcbaab
ababcabababc
同样绿色的不需要比较,是因为S[0]=T[0],S[0]=S[2],显然S[2]=T[0];同理,S[3]=T[1].但S[2]=T[2]],S[2]!=S[4],所以S[4]!=T[2],所以T串继续右移。
ababcabcaabcbaab
ababcabababc
所以得到了i指针不回溯,而j指针应该到j=2,然后S和T继续比较。那么j指针究竟应该回溯到几呢?我们假设next[j]保存了j指针回溯到的位置。那么KMP算法就得到了。(KMP代码给出后再解释next数组)。
int index_kmp(char s[], char t[], int next[])
{
int lens = strlen(s);
int lent = strlen(t);
int i = 0;
int j = 0;
while(i < lens && j < lent){
if(j == -1 || s[i] == t[j]){
i ++;
j ++;
}
else{
j = next[j];
}
}
if(j >= lent){
return i - lent; //输出的是目标串开始匹配字符的下标
}
else{
return -1;
}
}
ababcabcaabcbaab
ababcabababc
为什么j回溯到2呢,因为ababcab字符串的前缀ab=后缀ab,且是最大匹配长度,所以i指针不回溯,而j指针回溯到最大匹配长度。所以next的意义就是模式串T中j前面部分的前缀和后缀相等部分的长度,而且注意要是最大的。下面给出求next数组的函数。
void get_next(char t[], int *next)
{
int i, j;
int lent = strlen(t);
next[0] = -1;
i = 0;
j = -1;
while(i < lent){
if(j == -1 || t[i] == t[j]){
i ++;
j ++;
next[i] = j;
}
else{
j = next[j];
}
}
return ;
}
但是还有一种特殊情况需要考虑:
S=’aaabaaabaaabaaabaaab’
T=’aaaab’
-10 1 2 3 —-next数组
在这种情况下,红色不匹配,那么j指针回溯到next[3],即2。
aaabaaabaaabaaabaaab
aaaab
因为S[3]!=T[3], T[2]=T[3],显然S[3]!=T[2],所以其实这时j回溯到2是没必要的。指针j继续回溯。
aaabaaabaaabaaabaaab
aaaab
因为S[3]!=T[3], T[1]=T[3],显然S[3]!=T[1],所以其实这时j回溯到1是没必要的。指针j继续回溯。
aaabaaabaaabaaabaaab
aaaab
因为S[3]!=T[3], T[0]=T[3],显然S[3]!=T[0],所以其实这时j回溯到0也是没必要的。所以我们需要将next数组优化。
void get_nextval(char t[], int nextval[])
{
int i, j;
int lent = strlen(t);
i = 0;
nextval[0] = -1;
j = 0;
while(i < lent){
if(j == -1 || t[i] == t[j]){
i ++;
j ++;
if(t[i] != t[j]){
nextval[i] = j;
}
else{
nextval[i] = nextval[j];
}
}
else{
j = nextval[j];
}
}
}