子串的定位运算通常被称为串的模式匹配或串匹配,此运算应用非常广泛,搜索引擎、拼写检查、语言翻译、数据压缩等应用中,都有用到串匹配。
串的匹配通常设有两个字符串,主串S和子串T。在主串S中查找与模式T相匹配的子串,若匹配成功,返回子串第一的字符在主串S中的位置。
在分析KMP算法之前,先介绍一种简单明了算法——BF算法。
BF算法的算法思想是从主串S的第pos个字符起和模式串的第一个字符相比较,若相等,则逐个比较后续字符,否则从主串的下一个字符开始重新和模式串比较,以此类推,直至比完为止,并返回子串第一的字符在主串S中的位置,若匹配失败,返回 -1 。
typedef struct
{
char *ch;
int length;
}Hstring;
/*S为主串,T为子串,pos 通常为0*/
int BF_search(Hstring S, Hstring T, int pos)
{
int i = pos;
int j = 0;
while(i <= S.length && j <= T.length){
if(S[i] == T[j]){
i++;
j++;
}
else
i = i - j + 1;
j = 0;
}
if(j > T.length)//说明已经找到,跳出循环
return i - T.length;
else
return -1;
}
算法分析:
设主串为abcdecdabc
设子串为cda
主串abcdecdabcbcc
模式串cdabcb
第一趟:i = 0 ; j= 0
abcdecdabcbcc
c
a与c匹配失败
第二趟:i = 1 ; j = 0
abcdecdabcbcc
c
b与c匹配失败
第三趟:i = 2 ; j = 0
abcdecdabcbcc
c
c与c匹配成功
第四趟: i = 3 ; j = 1
abcdecdabcbcc
d
d与d匹配成功
第五趟: i = 4 ; j = 3
abcdecdabcbcc
a
e与a匹配失败
第七趟: i = 3 ; j = 0
abcdecdabcbcc
c
d与c匹配失败
以此类推,直至匹配成功
此时i = 11 ; j = 7
由此可知,子串在主串中位置为11-7+1=5
由上文可知,BF算法简洁明了,十分容易理解,但是当匹配失败时,总会有指针i回溯到i-j+1的位置,j也会随之回溯到j = 0的位置,因此,BF算法的时间复杂度高。而解决这个问题的,正是KMP算法。
KMP算法是由Knuth、Morris和Pratt同时设计的,相对于BF算法,其改进在于,每当一趟匹配过程出现字符比较不相等时,不需回溯i指针,而是利用已经得到的部分匹配的结果将模式串向右滑动尽可能远的一段距离后,继续进行比较。
int count = 0;//记录计算的次数
void makeNext(char P[],int next[])
{
int i,j;//i:模版字符串下标;j:最大前后缀长度
int P_len = strlen(P);//模版字符串长度
next[0] = 0;//模版字符串的第一个字符的最大前后缀长度为0
for (i = 1,j = 0; i < P_len; ++i)//for循环,从第二个字符开始,依次计算每一个字符对应的next值
{
while(j > 0 && P[i] != P[j]){//递归的求出P[0]···P[i]的最大的相同的前后缀长度j
j = next[j-1];
}
if (P[i] == P[j])//如果相等,那么最大相同前后缀长度加1
{
j++;
}
next[i] = j;
count ++;
}
}
int KmpSearch(char *s, char *p, int next[])
{
int i = 0, j = 0;
int slen=strlen(s);
int plen=strlen(p);
while(i < slen && j < plen){
if(j == 0 || s[i] == p[j]){
i++;
j++;
//printf("i_1 = %d\n",i);
//printf("j_1 = %d\n",j);
}
else{
j = next[j-1];
//printf("j_2 = %d\n",j);
}
count ++;
}
printf("j = %d\n",j);
if(j >= plen)
return i - plen;
else
return -1;
}
以上即为KMP算法的主体,KMP算法分为两部分,一部分是求next值,另一部分才是KMP。准确的说,KMP算法是在已知串的next函数值的基础上进行的。
(这里引用了 :http://blog.csdn.net/v_july_v/article/details/7041827)
算法分析
主串:abcdecdabcbcc
子串:cdabcb
共有元素长度(见下图):[0][0][0][0][1][0]
故得next 值:[0][0][0][0][1][0]
tips:部分匹配值 = 共有元素长度
第一趟:i = 0;j = 0
i++;j++;
第二趟:i = 1;j = 1
abcdecdabcbcc
d //不匹配
j = next[0] = 0;
第三趟:i = 1;j = 0
i++;j++
第四趟:i = 2;j = 1
abcdecdabcbcc
d//不匹配
j = next[1] = 0;
第五趟:i = 2; j = 0
abcdecdabcbcc
c//匹配
i++;j++
第六趟:i = 3; j = 1;
abcdecdabcbcc
d//匹配
i++;j++
第七趟:i = 4; j = 2
abcdecdabcbcc
a//不匹配
j = next[j] = 0
i_1 = 1
.....
第二十趟:i = 11; j = 6
abcdecdabcbcc
c//匹配
i++;j++
j >= plen,跳出循环
失配时,模式串向右移动的位数为:已匹配字符数 - 失配字符的上一位字符所对应的共有元素长度值,即保存在next数组中的部分匹配值,故可得 j - next[j]为右移的位数。
下面介绍部分匹配值(共有元素长度)是如何产生的:
首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。
next数组存储的数据是用来当模式串与主串不匹配的时候要模式串回退到第几个字符与主串再重新匹配,我们知道KMP算法的主串是不回朔的,当不匹配的时候我们不是退回到开始位置重新匹配,而是利用已经匹配的结果将模式串回朔到下一个位置,这个位置比开始位置更近一步;简单的说就是next[ j ]的值保存的是当模式串中第 j 个字符与主串第 i 个字符不匹配时候,模式串中的哪个字符重新与主串第 i 个再匹配,这样总的字符比较次数比从开始位置比较的次数就少了。
“部分匹配”的实质是,有时候,字符串头部和尾部会有重复。比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(”AB”的长度)。搜索词移动的时候,第一个”AB”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置。
检验程序
int main(void)
{
char s[] = "abcdecdabcbcc";
char p[] = "cdabcb";
int next[20];
int i, position;
next[0] = 0;
//若是去掉本行将会发生严重的错误
makeNext(p,next);
position = KmpSearch(s,p,next);
for(i = 0; i < strlen(p);i++)
printf("[%d]",next[i]);
printf("\n");
printf("position = %d\n",position);
printf("count = %d\n",count);
return 0;
}