串的模式匹配——KMP算法攻克

来源:互联网 发布:手机照一寸照的软件 编辑:程序博客网 时间:2024/04/30 14:36

这是我在CSDN上写的第一篇博客,哈哈,有点莫名的开心与紧张。

好吧,下面还是进入正题吧。

这个算法其实已经学习了好久,只是之前看了之后有点似懂非懂,知道后来看了严蔚敏老师的数据结构之后才算是攻克难关了。好开森~~下面且听我一一道来。


1.BF算法

我们把子串的定位操作称作模式匹配(设S为主串,T为子串)。先讲讲最基本的字符串匹配算法:BF算法(Brute-Force)BF算法思想:从主串S第i个字符起和模式串的第一个字符比较,若相等,则继续比较后续字符;否则主串指针回溯到i+1个字符开始重新和模式串比较。直到模式串和主串中的字符序列相等则匹配成功,返回主串匹配成功的位置。否则匹配失败,返回0。

BF算法的源码为1.1:

int BF(char S[], char T[])
{

int i = 0, j = 0;  //初始化匹配串和模式串的位置
while (S[i] != '\0'&&T[j] != '\0')
{
if (T[j] == S[i])
{
i++;
j++;
}
else
{
i=i-j+1;
j=0;
}
}
if ((T[j] == '\0'))
return (i-j+1);//返回本趟开始的位置(不是下标,下标应该减一)
else
return 0;
}

BF算法易于理解,但是其算法时间复杂度较高,为O(m*n)。考虑主串S长度为n,模式串T长度为m,在匹配成功的情况下考虑:

(1)最好情形:每趟不成功的匹配发生在模式 T的第一个字符。eg:S="aaaaaaaabc“,T=”bc“。

设匹配成功发生在si处,则在i-1趟不成功的匹配过程中共比较了i-1次,第i趟比较了m次,总共i-1+m次。所有匹配成功的位置为n-m+1种。设从si开始与模式T匹配成功的概率为pi,在等概率情况下,平均比较的次数是:

(2)最坏的情形:每趟不成功的匹配发生在模式T的最后一个字符。eg:S=”aaaaaaaab“,T="aaab"。

设匹配成功发生在si处,在i-1趟不成功的匹配中共比较了(i-1)*m次,第i趟成功的匹配共比较了m次,故共比较了i*m次,在等概率情况下平均的比较次数为:

因此BF算法的时间复杂度最差是O(n*m)。


2.KMP算法

考虑到BF算法的效率问题,KMP对此作了很大的改进,此算法可以在O(n+m) 的时间复杂度数量级上完成串的模式匹配。其基本思想:每趟比较不等时,主串不回溯!!而是利用已经得到的部分匹配的结果将模式串向右移动尽可能远的一段距离后继续比较。

设主串S=”S1S2...Sn“,模式串T=”P1P2...Pm“,在匹配的过程中需要解决的问题是:当失配(Si≠Pj)时,模式串向右移动可行的距离有多远,即主串中第i个字符(i指针不回溯)应与模式串T中的哪个字符比较?

假设与T中第k(k<j)个字符继续比较,则T中前k-1个字符的子串满足‘P1P2...Pk-1’=’Si-k+1 Si-k+1 ...Si-1‘。

而部分匹配结果是’Pj-k+1Pj-k+2 ...Pj-1‘=’Si-k+1 Si-k+1 ...Si-1‘。故:‘P1 P2...Pk-1’=’Pj-k+1 Pj-k+2 ...Pj-1‘。此式说明模式串中每一个字符对应一个k值,且这个k值仅依赖于模式串本身的字符序列,与主串无关。

若令next[j]=k,则next[j]表明当模式串中第j个字符与主串中相应字符失配时,在模式串中需重新和主串中该字符进行比较的字符的位置。next数组定义如下:

  

求得next函数之后,匹配过程如下:

若在匹配过程中,Si=Pj,则i、j各加1,否则i不变,j=next[j]再比较,以此类推。直至:(1)j退到某个next值(next[next[...[next[j]...)是字符比较相等,指针各自加1,继续匹配。(2)j退到-1(级模式串T的第一个字符失配),则从主串S的下一个字符Si+1起和模式串T重新匹配。

KMP算法的源码为1.2:

int KMP(char S[], char T[], char next[])
{
int i = 0, j = 0;  //初始化匹配串和模式串的位置
while (S[i] != '\0'&&T[j] != '\0')
{
if (j==-1||T[j] == S[i])
{
i++;
j++;
}
else
{

j = next[j];
}
}
if ((T[j] == '\0'))
return (i-strlen(T));
else
return 0;
}

算法复杂度为O(n+m)。

KMP算法在形式上与BF算法极其相似,不同之处在于:当失配时,主串指针i不回溯,模式串指针j退到next[j]再重新比较;当j=-1时,i和j各自增1。如KMP程序中的绿色部分所示。


3、next函数

由上可知,next函数只与模式串有关。我们可用递推的方法来求。

next[0]=-1,由定义可知。

设next[j]=k,则P1P2...Pk-1’=Pj-k+1Pj-k+2 ...Pj-1‘。其中1<k<j且不存在k'>k,使得此式成立。求next[j+1]=?

(1)若Pk=Pj,则P1P2...Pk’=Pj-k+1Pj-k+2 ...Pj‘,且不存在k'>k,使得此式成立,则next[j+1]=next[j]+1.

(2)若Pk≠Pj,则P1P2...Pk’≠Pj-k+1Pj-k+2 ...Pj‘。但是,这是我们可以把求next函数值的问题看成一个模式匹配的问题,整个模式串T既是主串又是模式串,若pkpj,需将模式串向右移动至第next[k]个字符位置和主串中的第j个字符比较。

设next[k]=k',若此式Pk'=Pj,则说明主串中第j个字符之前存在一个长度为k'的最长子串,和模式串中从首字符起长度为k'的子串相等P1P2...Pk'’=Pj-k'+1Pj-k'+2 ...Pj‘,那么,next[j+1]=k'+1=next[k]+1。同理,若Pk'Pj,将模式串继续向右移动直至将模式中第next[k']个字符和pj对齐,。。。,知道pj和模式串中某个字符匹配成功或不存在任何k',则next[j]=0。

分析了这么多next函数的递推式求法,上源码1.3:

void Next(char T[],char next[])
{
int j = 0,k = -1;
next[0] = -1;
while (T[j] != '\0')
{
if (k == -1 || T[j] == T[k])
{
j++;
k++;
next[j] = k;
}
else
{
k = next[k];
}
}
}

算法时间复杂度为O(m)。


4.next函数改进

前面讲的next算法在以下情况下存在缺陷,eg:S=“aaabaaaab”,T=“aaaab”,匹配过程中,当i=3,j=3时,失配,由之前得到的next[j]的指示还需进行i=3 j=2,i=3 j=1和i=3 j=0三次比较,但是因为模式串中的第0,1,2个字符与第3个字符相等,显然不需要再和主串的第3个字符比较,而可以一下将模式串向右移动4个字符的位置直接进行i=4 j=0的字符比较。即,若模式中Pk=Pj,当主串字符Si与Pj比较不等时,不许再和Pk比较,直接与Pnext[k]比较。得到next函数的修正值算法,源码为1.4:

void Nextval(char T[],char next[])
{
int j = 0,k = -1;
next[0] = -1;
while (T[j] != '\0')
{
if (k == -1 || T[j] == T[k])
{
j++;
k++;
if (T[j] != T[k])
{
nextval[j] = k;
}
else
{
nextval[j] = nextval[k];
}

}
else
{
k = nextval[k];
}
}
}

它和前一个next函数的区别我已经用红色部分和蓝色部分标注出来,再根据之前的讲解,应该很容易区分。


以上四个算法,本质上都是一个框架,只要知道了一个算法,然后记住它们之间的差别,四个算法就没有问题!

程序看起来很简单易懂,不过要真正理解透彻,真心不容易。网上类似的资源虽然很多,不过千差万别,最后我还是通过书本一点点将难题攻克,在此纪念下!严蔚敏老师的数据结构帮了大忙了。

0 0
原创粉丝点击