1.选题背景:
模式匹配类算法在我们的工作、学习中应用十分的广泛,KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,主要用于串的模式匹配。简单的暴力匹配算法主串S和模式串T会因为匹配不成功而造成回溯的情况,这样会大大的加大了算法的时间复杂度,它的最坏时间复杂度为O(mn)。KMP算法的核心思想是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。它能将时间复杂度降到O(m+n)。因此本课题解决的主要问题是KMP的算法的设计实现以及进一步的优化。
2.原理介绍:
要了解KMP算法的高效,就需要与暴力匹配算法进行对比。当没有一个好的解决办法时,暴力匹配方法时我们大多数人最先想到的。但是在解决复杂情况、数据量较大等问题时,就需要设计出更好的算法来提升代码的效率。
2.1暴力匹配的原理
暴力匹配的部分代码:
int Index(SString S,SString T){
int i=1,j=1;
while(i<=S.length&&j<=T.length){
if(S.ch[i]==T.ch[j]){
++i;++j; //匹配相等,继续比较后继字符
}
else{
i=i-j+2;
j=1;
//指针后退重新开始匹配
}
}
if(j>T.length ) return i-T.length;
else return 0;
}
i和j分别表示主串S和模式串T当时的字符位置。算法的思想为从主串S的第一个字符起,与模式T的第一个字符比较,若相等,就继续逐个的比较后面的字符;否则从主串的下一个字符起,重新和模式的字符比较。直到模式T中的每个字符依次和主串S中的一个连续的字符串序列相等,就匹配成功,函数值为与模式T中第一个字符相等的字符在主串S中的序号,否则就匹配失败,函数的返回值为0。
图1.1
可以看到因为存在回溯,暴力匹配法的最坏时间复杂度O(nm)。通过观察我们发现可以分析模式本身的结构入手,如果已匹配相等的前序序列中有某个后缀正好时模式的前缀,那么就可以通过将模式向后滑动到这些相等字符对齐的位置,主串i的指针不会回溯,并从该位置继续开始比较。模式向后移动的位置的计算与模式本身的结构有关,与主串无关。
2.2KMP算法匹配的原理
KMP 的高效在于在匹配过程中失配的情况下,将模式串T有效地多往后面跳几个字符,加快匹配速度。KMP算法认为,匹配过程与模式串的特征关系密切,如果对于模式串中存在一个整数K(K<j),使得模式串T中第K个字符之前的K个字符(T[0],T[1]...T[K-1])依次与T[j]的前面K-1个字符(T[j-K]T[j-K+1]...T[j-1])相同,并与主串S中第i个字符之前的K个字符相等,那么匹配仅需要从模式串T中的第K个字符与主串的第i个字符起继续比较。也就是KMP算法为什么高效的原因。
- 程序设计与分析:
KMP模式匹配部分代码:
int Index_KMP(String S,String 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]){
++i;++j;
//匹配相等,继续比较后继字符
}
else//模式串向右移动
j=next[j];
}
if(j>T.length)//匹配成功
return i-T.length;
else
return 0;
}
当然我们此时仅仅只是知道模式T需要进行移动,并不知道模式串T怎么进行移动,需要移动多少位等一些问题。这就需要使用next数组来分析模式串T。next[j]是表示了T串中的第j个字符与主串S中相应字符失配时,在模式串T中需要重新和主串S中该字符进行比较的字符位置。通过查阅资料和网上学习我大致了解到KMP模式匹配失配大致有四种情况:
第一种情况是T串在失配字符前没有重复字符的情况时,第一次匹配是S串iamhappy与T串iamhc在下标为5的元素失配时(即S[5]!=T[5]),可以看到T串失配字符前的几个元素发现并不相等,即T[1]≠T[2]≠T[3]≠T[4],所以可以判断出t[1]字符与s串的前四个字符都不相同,所以第二次比较直接将T[1]字符与S[5]字符开始比较即可,子串向右滑动了4个字符。
第二种情况是T串在失配字符前有一个字符重复时S串www.baidu与T串ww.在下标为3的位置失配时,按照第一种情况应该把S[3]与T[1]匹配,匹配不成功,但却会错过它的正确匹配的位置,即S[2]与T[1]开始进行匹配后发现,S[2]=T[1]、S[3]=T[2]、S[4]=T[3],模式串向右滑动了1个字符。
第三种情况是T串在失配字符前有两个字符重复时: S串absabsqueen和T串absabc在下标为6的位置失配时,通过观察会发现在T串失配字符前有两个字符是重复的,即T[1]=T[4]、T[2]=T[5],而T[4]=S[4]、T[5]=S[5],所以T[1]=S[4]、T[2]=S[5],T[3]和S[6]匹配,要经过比较后才知道,所以不能放过任何一种可能性,应该尝试把重复部分的后一个字符与S[6]进行匹配,模式串t向右滑动了5个字符。
最后一种情况是T串在失配字符前所有的字符都相同时:对于特殊字符S串ccccanmx和T串ccccb,在失配的下标为5的位置的字符前的字符串有最长四个字符重复,可以直接用T[4]号元素与S串失配的字符进行匹配,但是发现T[4]与S[5]仍然失配并且T[1]=T[2]=T[3]=T[4],所以就直接将T[1]与是S[6]进行匹配。
因此我们通过分析这四种情况编写一个get_next()的函数,来计算输入模式串T的next数组值。
next数组的代码如下:
void get_next(String T,int next[])
{
int i=1,j=0;
next[1]=0;
while(i<T.length){
if(j==0||T.ch[i]==T.ch[j]){
++i;
++j;
next[i]=j; //若Pi=Pj,则next[j+1]=next[j]+1
}
else
j=next[j]; //否则就让j=next[j],继续循环
}
}
通过上述代码的大致思想就让我们之前所说的0(mn)的复杂度降到了O(n+m),KMP算法现在在模式匹配中仍然是广泛采用的代码。当然上面的算法通过测试发现显然不是最好的,因为我们前面有讨论过T串在失配字符前所有的字符都相同时的这种情况,上面的代码显然没有考虑到这一种情况。但是总体来说已经实现了不回溯。下面就写出正对这一个问题的解决代码。
改进后的next2数组的部分代码如下:
void get_next2(String T,int next2[]){
int i=1,y=0;
next2[1]=0;
while(i<T.length){
if(j==0 ||T.ch[i]==T.ch[j]){
++i;
++j;
if(T.ch[i]!=T.ch[j]) next2[i]=j;
else next2[i]=next2[j];
}
else
j=next2[j];
}
}
- 代码模块:
一个简单的BF暴力匹配的全代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define NotFound -1//宏定义
int Index(char S[], char T[]) {
int i = 0, j = 0;
int len1 = strlen(S);
int len2 = strlen(T);
while (i < len1 && j < len2) {
if (S[i] == T[j]) {
++i;++j; //匹配相等,继续比较后继字符
}
else {
i = i - j + 1;
j = 0;
//指针后退重新开始匹配
}
}
if (j >= len2) return i - len2 + 1;
else return NotFound;
}
int main()
{
char S[] = "ababcabcacbab";
char T[80];
scanf("%s", T);
int p = Index(S, T);
if (p == NotFound) printf("Not Found.\n");
else {
printf("查找成功\n");
printf("匹配成功的起始位置%d\n", p);
}
return 0;
}
一个简单的KMP匹配的全代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define NotFound -1//宏定义
void get_next(char *t, int *next)//旧的next数组
{
int i, j;
int m = strlen(t);
next[0] = -1;//这里把数组先赋值为1
for ( j=1; j<m; j++ ) {
i = next[j-1];
while ( (i>=0) && (t[i+1]!=t[j]) )
i = next[i];
next[j] = -1;
}
}
void get_next2(char *t, int *next)//新的next数组
{
int i, j;
int m = strlen(t);
next[0] = -1;
for ( j=1; j<m; j++ ) {
i = next[j-1];
while ( (i>=0) && (t[i+1]!=t[j]) )
i = next[i];
if ( t[i+1]==t[j] )//解决设计分析种探讨过的第四种匹配失衡的情况
next[j] = i+1;
else next[j] = -1;
}
}
int KMP( char *string, char *t )//KMP模式匹配算法
{
int n = strlen(string);
int m = strlen(t);
int s, p, *next;
if ( n < m ) return NotFound;
next = (int *)malloc(sizeof(int) * m);
get_next(t, next);
//get_next2(t, next);
s = p = 0;
while ( s<n && p<m ) {
if ( string[s]==t[p] ||p==-1) {
s++; p++;//匹配相等,继续比较后继字符
}
else if (p>0) p = next[p-1]+1;
else s++;
}
return ( p==m )? (s-m) : NotFound;
}
int main()
{
char string[] = "Tfsdfsffdssf his is a simpssdale a edasxample.baaa";//先输入的主串S
char t[80];
scanf("%s",t);//输入需要拼配字符串
int p = KMP(string, t);
if (p==NotFound) printf("Not Found.\n");
else {printf("查找成功\n");
printf("%s\n", string+p);
}
return 0;
}