KMP模式匹配算法

 

关于KMP算法本身已经没有好说的了,网上资料也比较多,我看的是下面这段网页的内容,跟书上差不多:

模式匹配的KMP算法详解
点击数:12442    发布日期:2006-6-12 16:33:00  
【收藏】 【评论】 【打印】 【编程爱好者论坛】 【关闭】

Tag:KMP 模式匹配 
 
模式匹配的KMP算法详解

这种由D.E.Knuth,J.H.Morris和V.R.Pratt同时发现的改进的模式匹配算法简称为KMP算法。大概学过信息学的都知道,是个比较难理解的算法,今天特把它搞个彻彻底底明明白白。

注意到这是一个改进的算法,所以有必要把原来的模式匹配算法拿出来,其实理解的关键就在这里,一般的匹配算法:

int Index(String S,String T,int pos)//参考《数据结构》中的程序
{
  i=pos;j=1;//这里的串的第1个元素下标是1
  while(i
< =S .Length && j< =T.Length)
  
{
    if(S[i]
==T[j]){++i;++j;}
    
else{i =i-j+2;j=1;}//**************(1)
  
}
  if(j
> T.Length) return i-T.Length;//匹配成功
  else return 0;
}

匹配的过程非常清晰,关键是当‘失配’的时候程序是如何处理的?回溯,没错,注意到(1)句,为什么要回溯,看下面的例子:

S:aaaaabababcaaa  T:ababc

aaaaabababcaaa
    ababc.(.表示前一个已经失配)
回溯的结果就是
aaaaabababcaaa
     a.(babc)
如果不回溯就是
aaaaabababcaaa
        aba.bc
这样就漏了一个可能匹配成功的情况
aaaaabababcaaa
      ababc

为什么会发生这样的情况?这是由T串本身的性质决定的,是因为T串本身有前后'部分匹配'的性质。如果T为abcdef这样的,大没有回溯的必要。

改进的地方也就是这里,我们从T串本身出发,事先就找准了T自身前后部分匹配的位置,那就可以改进算法。

如果不用回溯,那T串下一个位置从哪里开始呢?

还是上面那个例子,T为ababc,如果c失配,那就可以往前移到aba最后一个a的位置,像这样:
...ababd...
   ababc
   ->ababc

这样i不用回溯,j跳到前2个位置,继续匹配的过程,这就是KMP算法所在。这个当T[j]失配后,j应该往前跳的值就是j的next值,它是由T串本身固有决定的,与S串无关。

《数据结构》上给了next值的定义:
          0   如果j=1
next[j]={Max{k|1
< k <j且'p1...pk-1' ='pj-k+1...pj-1'
          
1   其它情况

我当初看到这个头就晕了,其实它就是描述的我前面表述的情况,关于next[1]
=0是规定的,这样规定可以使程序简单一些,如果非要定为其它的值只要不和后面的值冲突也是可以的;而那个Max是什么意思,举个例子:

T:aaab

...aaaab...
   aaab
  -
> aaab
   ->aaab
    ->aaab

像这样的T,前面自身部分匹配的部分不止两个,那应该往前跳到第几个呢?最近的一个,也就是说尽可能的向右滑移最短的长度。

OK,了解到这里,就看清了KMP的大部分内容,然后关键的问题是如何求next值?先不管它,先看如何用它来进行匹配操作,也就是说先假设已经有了next值。

将最前面的程序改写成:

int Index_KMP(String S,String T,int pos)
{
  i=pos;j=1;//这里的串的第1个元素下标是1
  while(i
< =S .Length && j< =T.Length)
  
{
    if(j
==0  || S[i] ==T[j]){++i;++j;}  //注意到这里的j ==0,和++j的作用就知道为什么规定next[1]=0的好处了
    
else j =next[j];//i不变(不回溯),j跳动
  
}
  if(j
> T.Length) return i-T.Length;//匹配成功
  else return 0;
}

OK,是不是非常简单?还有更简单的,求next值,这也是整个算法成功的关键,从next值的定义来求太恐怖了,怎么求?前面说过了,next值表达的就是T串的自身部分匹配的性质,那么,我只要将T串和T串自身来一次匹配就可以求出来了,这里的匹配过程不是从头一个一个匹配,而是从T[1]和T[2]开始匹配,给出算法如下:

void get_next(String T,int 
&next [])
{
  i=1;j=0;next[1]=0;
  while(i
< =T .Length)
  {
    if(j
==0  || T[i] ==T[j]){++i;++j;  next[i] =j;/**********(2)*/}
    
else j =next[j];
  
}
}

看这个函数是不是非常像KMP匹配的函数,没错,它就是这么干的!注意到(2)语句逻辑覆盖的时候是T[i]
==T[j]以及i前面的、j前面的都匹配的情况下,于是先自增,然后记下来next[i]=j,这样每当i有自增就会求得一个next[i],而j一定会小于等于i,于是对于已经求出来的next,可以继续求后面的next,而next[1]=0是已知,所以整个就这样递推的求出来了,方法非常巧妙。

这样的改进已经是很不错了,但算法还可以改进,注意到下面的匹配情况:

...aaac...
   aaaa.
T串中的'a'和S串中的'c'失配,而'a'的next值指的还是'a',那同样的比较还是会失配,而这样的比较是多余的,如果我事先知道,当T[i]
==T[j],那next[i]就设为next[j],在求next值的时候就已经比较了,这样就可以去掉这样的多余的比较。于是稍加改进得到:

void get_nextval(String T,int &next[])
{
  i
=1;j=0;next[1]=0;
  
while(i< =T.Length)
  
{
    if(j
==0  || T[i] ==T[j])
    
{ ++i;++j;
      if(T[i]!
=T[j])  next[i] =j;
      
else next[i] =next[j];//消去多余的可能的比较,next再向前跳
    
}
    else j
=next[j];
  
}
}

匹配算法不变。

到此就完全弄清楚了,以前老觉得KMP算法好神秘,真不是人想出来的,其实不然,它只不过是对原有的算法进行了改进。可见基础的经典的东西还是很重要,你有本事‘废’了经典,就创造了进步。

2006-06-12 rickone

KMP算法的核心,我觉得的应该是对模式串进行分析,计算局部信息,然后在与主串进行匹配时,利用这些信息减少比较的次数。下面这个问题就是利用KMP的这个思想来做的,当然代码肯定不一样。问题来源于别人的一篇论文,看了别人的解答,觉得学习了不少。

问题:字符串T和S, 求函数Bi = |TS(i,n)的最长公共前缀|,这里 1<=i<n。,T的长度m, S的长度n

 直观方法,直接比较,复杂度O(n * m)

考虑假设我们要求T与S(i,n)的最长公共前缀,并且已经求出了所有的B(k) 1 <=k <i,如何利用已经求出的信息来减少我们求新的最长公共前缀时的比较次数呢。

定义如下函数A(i) = |T与T(i,m)的最长公共前缀|

利用递推的方法进行求解。初始情况求A(2)时,直接进行比较。

假设A(2),A(3)...A(i-1)均已求出,其中k = Max{A(k)|1<=k<i}。

设Len = k + A(k) - 1

    L = A(-k+1)。 为什么看i+k-1这项呢,当初考虑了一下,画了图才一目了然。

   Seq0: T(k) T(k+1) ...T(i)............T(Len)

   Seq1: T(1) T(2).........T(i-k+1)...T(Len-k+1)

   Seq3:                          T(1) T(2)....T(L).

通过T(i-k+1),T(i) 就与T(1)发生关系了,挺巧妙吧!

具体求的时候需要分情况讨论:

第一种情况: i < Len

(1)Len - i + 1 > L,  不需要比较了,A(i) = L。因为T(i+L-1) = T(L), 但是T(i+L) = T(i-k+1+L) != T(L+1), 所以A(i) = L

(2)Len - i + 1 <= L, 这个不等式说明{T(i).....T(Len)} ={T(1).....T(Len-i+1)},从T(Len+1)开始的剩余字符串尚不能确定,因此可以令j = Len - i + 1 ,然后T(i+j)与T(1+j)进行逐个比较直到遇到不相等字符,最后A(i) = j

第二种情况:i > Len

以前的信息无法帮助求A(i), 因此就需要从T的开始一个一个的比较,直到遇到不相等的字符。

如果T老是出现第二种情况,那么它的复杂度肯定就与一个一个的匹配差不多,但是由于字符串中存在着重复子串,因此可以利用以前求过的值来减少比较的次数。

那么求T与S(i)的最长公共前缀,就如出一辙了。我们先求T与S(1)的最长公共前缀B(0), 然后采用递推的方法求B(i)。求B(i)时,像上面一样,画个图,把思路弄清楚:

假设B(2),B(3)...B(i-1)均已求出,其中k = Max{B(k)|1<=k<i}。

设Len = k + B(k) - 1

    L = A(i-k+1)。 i < Len的情况

   Seq0: S(k) S(k+1) ...S(i)............S(Len)

   Seq1: T(1) T(2).........T(i-k+1)...T(Len-k+1)

   Seq3:                          T(1) T(2)....T(L).

关于这个问题我的源代码如下:

#define  MAXSTRLENGHT 1000000

int  T[MAXSTRLENGHT];
int  S[MAXSTRLENGHT];

// 求T与T(i,m)的最长公共前缀
void  getTlongestPrefix( const   char   * moldStr)
...

// 求T与S(i,n)的最长公共前缀
void  getLongestPrefix( const   char   * mainStr,  const   char   * moldStr)
{
    
int i,j,k;

    
for(j=0;mainStr[j] == moldStr[j];j++);

    S[
0= j;

    k 
= 0;

    
for(i = 1;i<(int)strlen(mainStr);i++)
    
{
        
int len = k + S[k] -1;
        
int L = T[i-k];

        
if(i < len)
        
{
            
if(L < len - i + 1)
                S[i] 
= L;
            
else
            
{
                j 
= len - i + 1;
                
while(mainStr[i+j]==moldStr[j])
                    j
++;
                S[i] 
= j;
            }

        }

        
else
        
{
            j 
= 0;
            
while(moldStr[j] == mainStr[i+j])
                j
++;
            S[i] 
= j;
        }


        
if(S[i] > S[k])
            k 
= i;
    }

}

这个算法是参照一篇论文的,觉得对KMP讨论地不错,就记下来了。

参考链接为:http://acm.oopos.cn/maxstring.htm

此外还收集了另外一个与KMP算法相关的问题。已知字符串S和P,要求最大的k,使得P的前k个字符是S的后缀。

直观方法就是从k=1开始,依次比较,直到P的前k个字符是S的后缀,P的前k+1个字符不是S的后缀。最坏情况下

算法复杂度为O(n2),其中n是P的长度。这种方法是S串从右边向左边扩展,P串从左边向右边扩展。借鉴KMP算法

思想对字符串比较方式进行修改,使得S和P串均从左边向右边扩展。

从i=0开始,依次求S的子串S[0...i]与P的最大k。假设已经求出S[i]对应的k,接下来需要求S[i+1]对应的k。

 S[0],S[1],...S[i],    S[i+1]

P[0],P[1],....P[k-1] P[k]

If(S[i+1]==P[k])

   S[i+1]对应的k=(S[i]对应的k)+ 1

else

   将P看作KMP算法中的模式串,S[0..i+1]看作KMP算法中的主串,S[i+1] != P[k]相当于模式串P在位置k失配,

   因此令q=k, 然后求q =next[q],直到P[q]与S[i+1]匹配,或者q==-1,即P的任何长度的前缀均不能同S[0]...S[i+1]

   匹配,S[i+1]对应的k = q + 1。

具体实现代码如下:

#include <iostream>
#include <string>
using namespace std;
//计算最大的k,满足P的前k个字符是S的后缀
int suffixFunc(const string& S,const string& P)
{
 int pl = P.length();
 int* next = new int[pl+1];
    //计算next
 int j = 0;
 int k = -1;
 next[0] = -1;
 while(j < P.length())
 {
  if(k == -1 || P[j] == P[k])
  {
   j++;
   k++;
   if(P[j]!=P[k])
    next[j] = k;
   else
    next[j] = next[k];
  }
  else
  {
   k = next[k];
  }
 }
 int n = 0;
 int q = 0;
 for(int i = 0; i < S.length();)
 {
  while(true)
  {
   if(q == -1 || q <= P.length() && P[q] == S[i])
   {
    n = q + 1;
    i++;
    break;
   }
   q = next[q];
  }
  q = n;
 }
 delete[] next;
 return n;
}
int main(int argc,char **argv)
{
 string s,p;
 while(cin>>s>>p)
 {
  cout<<suffixFunc(s,p)<<endl;
 }
}

而这个问题的解答可以帮助求解下面一个问题:

将一串字符放在一个环上,那么从环上字符串的任意一个位置起遍历一周可以形成对应的字符序列

给出两个这样的字符序列

要求判断两个字符序列是否对应于同样的一串字符

如abc与bca两个字符序列表示同样一串字符

假设两个串分别为S和P,先用上面的方法求得最大的k,P的前k个字符是S的后缀,然后比较S[0..S.length-k-1]与

P[k...P.length-1]是否相同,如果相同则两个字符序列表示同一串字符。

整个源代码如下:

#include <iostream>
#include <string>
using namespace std;
//计算最大的k,满足P的前k个字符是S的后缀
int suffixFunc(const string& S,const string& P)
{
 int pl = P.length();
 int* next = new int[pl+1];
    //计算next
 int j = 0;
 int k = -1;
 next[0] = -1;
 while(j < P.length())
 {
  if(k == -1 || P[j] == P[k])
  {
   j++;
   k++;
   if(P[j]!=P[k])
    next[j] = k;
   else
    next[j] = next[k];
  }
  else
  {
   k = next[k];
  }
 }
 int n = 0;
 int q = 0;
 for(int i = 0; i < S.length();)
 {
  while(true)
  {
   if(q == -1 || q <= P.length() && P[q] == S[i])
   {
    n = q + 1;
    i++;
    break;
   }
   q = next[q];
  }
  q = n;
 }
 delete[] next;
 return n;
}
//将一串字符放在一个环上,那么从环上字符串的任意一个位置起遍历一周可以形成对应的字符序列
//给出两个这样的字符序列
//要求判断两个字符序列是否对应于同样的一串字符
bool inSameRing(const string& S, const string& P)
{
 bool sameRing = false;
 if(S.length() == P.length())
 {
  int l = suffixFunc(S,P);
  string &s = S.substr(0,S.length()-l);
  string &p = P.substr(l);
  sameRing = (s==p);
 }
 return sameRing;
}
int main(int argc,char **argv)
{
 string s,p;
 while(cin>>s>>p)
 {
  cout<<suffixFunc(s,p)<<endl;
  cout<<inSameRing(s,p)<<endl;
 }
 return 0;
}

关于最后一个问题,如果有什么更好的算法,请赐教,谢谢!

一起学习!

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值