KMP算法

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算法为什么高效的原因。

  1. 程序设计与分析

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];

}

}

  1. 代码模块:

一个简单的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;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值