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算法好神秘,真不是人想出来的,其实不然,它只不过是对原有的算法进行了改进。可见基础的经典的东西还是很重要,你有本事‘废’了经典,就创造了进步。

 ================================================================================

 

我们从一个普通的串的模式匹配算法开始讲起,这样你才能更深入的了解KMP算法及其优点。
咱们先来看看普通的串的模式匹配算法是怎么进行比较的

主串 (S) a b a b c a b c a c b a b
子串 (T)a b c a c    (子串又被称为模式串)

红色表示当前这趟比较指针所在位置,兰色表示当前这趟比较中匹配的部分

第一趟(详细过程)

a b a b c a b c a c b a b
a b c a c


a b a b c a b c a c b a b
a b c a c


a b a b c a b c a c b a b
a b c a c
遇到不匹配的地方时指针回朔,子串向前移动一位(下同),变成如下形式

a b a b c a b c a c b a b
  a b c a c

第二趟(省略了中间阶段指针移动比较过程,下同)

a b a b c a b c a c b a b
  a b c a c

第三趟

a b a b c a b c a c b a b
    a b c a
c

第四趟

a b a b c a b c a c b a b
      a b c a c

第五趟

a b a b c a b c a c b a b
        a b c a c

第六趟

a b a b c a b c a c b a b
          
a b c a c _
完成匹配,跳出

这就是普通算法的详细匹配过程,看明白了算法就简单了
详细算法我现在就不给了,等以后有时间再编辑。不过假如串的长度为m,子串的长度为n的话,那么这个算法在最坏的情况下的时间复杂度为O(m*n) ,有没有办法降低它的时间复杂度呢?(废话,当然有拉,不然回这个帖子干什么
拜D.E.Knuth 和 J.H.Morris 和 V.R.Pratt  所赐,我们有了一种时间复杂度为O(m+n)的算法,为了纪念这3位强人为计算机科学所做的贡献,分别取这3位先生的名字的首写字母K,M,P来命名这个算法,即著名的KMP算法。

我们先不管这个KMP算法是什么,我们先来看看我们能够想到怎样的方法来改进上面的普通算法。

通过观察,我们发现一个问题,如果一个子串,假设为a b c d e f , 兰色的部分与主串匹配, 红色的f 与主串不匹配,那么这个子串最多能往右边移动几位呢?因为子串中的第一个字符 a!=b !=c !=d !=e !=f ,那么主串中与b c d e 匹配的部分肯定和a不匹配而与
f不匹配的那部分无法判断与a是否匹配。因此子串最多向右移动5位, 即a移动到f所在的位置上,再进行判断。

在解决这个问题的同时,我们又发现了一个问题,当这个子串为a b c d a f 时又会如何呢?因为无法判断主串中与a匹配对应的位置开始往后的部分是否与整个子串相匹配,所以子串最多向右移动4位,即a移动到a的位置上,然后再进行判断。

按照这个方法,我们再来看看前面我们举的例子。

第一趟

a b a b c a b c a c b a b
a b c a c

第二趟

a b a b c a b c a c b a b
    a b c a c

第三趟

a b a b c a b c a c b a b
          a b c a c _
完成匹配,跳出

是不是简单多了呢?这个方法不困难吧,也很容易理解吧。
事实上这就是很多人大叫困难的KMP算法的最基本也是最核心的方法
你现在是不是在想 如果我早生50年,KMP算法就要改名了呢?

当然,强人们的思维比我们严密多了,他们考虑的更完全,比如象 a b c d e a b c  ,这种字符串相同的情况,当然这种情况并不比单个字符相同的情况复杂多少。在思想上KMP算法与上面我们讲的方法完全一致,都是要让子串向右移动尽可能远的一段距离来达到降低时间复杂度的目的,但在具体操作上KMP算法与上面的方法又有所不同。他们为子串引入了一个参数next[ j ] ,我们先来讲下next[j] 怎么求:

假设子串为 'p1 p2 p3 p4.......pm '
对于第j个字符,有next[ j ] =  
           (1)   0    (j=1)
           (2)   Max{ k | 1<k<j  &&  ' p1...p k-1 '  = ' p j-k+1...p j-1 ' }
           (3)   1    其他情况

没有大括号就是不方便。哎,上面的算式是不是把你弄晕了呀?没关系,下面我介绍一种偷懒的方法

我就以

子串     a b a b a a b a b
next[j]  0 1 1 2 3 4 2 3 4

为例 来讲下
next[j] 里面 开头的红色01 是固定格式.

我就以兰色的4 来说明下为什么是4.
与4有关的, 是4所对应的兰色a之前的所有的字符,即紫色的 a b a b a
这个字符串中所有符合匹配条件的字符串如下
a b a b a             a b a b a
a                             a
a b a                     a b a    最长的匹配字符串(a b a b a本身除外)在这里 ,长度为3, 再加上1,就是4
注意next[ j ]中4 的位置!
next[ j ]中其他位置的数值也可以用同样的方法得出

为什么next[j]开头为固定的01?这个问题不太好回答。
最开头的next[j]是由子串第一个字符前面的字符来决定的,但那不存在,因此第一个next[j]的值为0
第二个next[j]是由第一个字符决定的,由于他是自己与自己比较,一定相等,所以值为1。
这样说可能有点牵强。那么我们来看看下面,可能会明白些,也更加了解next[j]到底是起什么作用的。

子串                   a b a b a a b a b
next[j]                0 1 1 2 3 4 2 3 4
匹配子串长度           1 2 3 4 5 6 7 8 9   (对应位置  也可看成是截止到当前字符的子串串长)
最大右移               1 1 2 2 2 2 5 5 5   (当前字符不匹配时,子串最大右移距离)

我们发现,最大右移 + 对应的next[j] = 匹配子串长度,我估计这就是为什么要引入next[ j ] 这个参量,为了方便计算最大右移。不过对于next[j]的官方具体定义我没找到,望高人相告,在下先谢过了。

事实上,KMP算法还是有改进余地的,我们一直都在避免讨论这样一种情况:
a b c d e a
0 1 1 1 1 1
a不匹配时,能向右最大移动几位呢?答案是6位,a 所能移动到的位置为a的下一位,即a 的右边一位
而根据next[j]计算出子串最大右移为6-1=5。这里他将a移动到a的位置上,然后做了一次无意义的比较(a = a  ,a 与主串不匹配,那么显然a 与主串也不匹配)。这个问题我就不说了,虽然我有个改进方法,但这个问题还是留给大家去思考吧。

至此,关于KMP算法的讲解就到此为止了,如果你还没弄明白KMP是什么,我就没办法了。什么?没给出算法?自己翻书去,我把比算法还重要的东西都给出来了,算法还不是小意思
我相信即使你现在还没明白KMP 是怎么回事(什么?你还没明白?), 结合书上的算法,你也会很快明白的。
===================================================================================

如果机房马上要关门了,或者你急着要和MM约会,请直接跳到第六个自然段。

       我们这里说的KMP不是拿来放电影的(虽然我很喜欢这个软件),而是一种算法。KMP算法是拿来处理字符串匹配的。换句话说,给你两个字符串,你需要回答,B串是否是A串的子串(A串是否包含B串)。比如,字符串A="I'm matrix67",字符串B="matrix",我们就说B是A的子串。你可以委婉地问你的MM:“假如你要向你喜欢的人表白的话,我的名字是你的告白语中的子串吗?”
       解决这类问题,通常我们的方法是枚举从A串的什么位置起开始与B匹配,然后验证是否匹配。假如A串长度为n,B串长度为m,那么这种方法的复杂度是O (mn)的。虽然很多时候复杂度达不到mn(验证时只看头一两个字母就发现不匹配了),但我们有许多“最坏情况”,比如,A= "aaaaaaaaaaaaaaaaaaaaaaaaaab",B="aaaaaaaab"。我们将介绍的是一种最坏情况下O(n)的算法(这里假设 m<=n),即传说中的KMP算法。
       之所以叫做KMP,是因为这个算法是由Knuth、Morris、Pratt三个提出来的,取了这三个人的名字的头一个字母。这时,或许你突然明白了AVL 树为什么叫AVL,或者Bellman-Ford为什么中间是一杠不是一个点。有时一个东西有七八个人研究过,那怎么命名呢?通常这个东西干脆就不用人名字命名了,免得发生争议,比如“3x+1问题”。扯远了。
       个人认为KMP是最没有必要讲的东西,因为这个东西网上能找到很多资料。但网上的讲法基本上都涉及到“移动(shift)”、“Next函数”等概念,这非常容易产生误解(至少一年半前我看这些资料学习KMP时就没搞清楚)。在这里,我换一种方法来解释KMP算法。

       假如,A="abababaababacb",B="ababacb",我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当 i=j=5时的情况。

       i = 1 2 3 4 5 6 7 8 9 ……
       A = a b a b
a b a a b a b …
       B = a b a b
a c b
       j = 1 2 3 4
5 6 7

       此时,A[6]<>B[6]。这表明,此时j不能等于5了,我们要把j改成比它小的值j'。j'可能是多少呢?仔细想一下,我们发现,j'必须要使得B[1..j]中的头j'个字母和末j'个字母完全相等(这样j变成了j'后才能继续保持i和j的性质)。这个j'当然要越大越好。在这里,B [1..5]="ababa",头3个字母和末3个字母都是"aba"。而当新的j为3时,A[6]恰好和B[4]相等。于是,i变成了6,而j则变成了 4:

       i = 1 2 3 4 5 6 7 8 9 ……
       A = a b a b a
b a a b a b …
       B =        a b a
b a c b
       j =        1 2 3
4 5 6 7

       从上面的这个例子,我们可以看到,新的j可以取多少与i无关,只与B串有关。我们完全可以预处理出这样一个数组P[j],表示当匹配到B数组的第j个字母而第j+1个字母不能匹配了时,新的j最大是多少。P[j]应该是所有满足B[1..P[j]]=B[j-P[j]+1..j]的最大值。
       再后来,A[7]=B[5],i和j又各增加1。这时,又出现了A[i+1]<>B[j+1]的情况:

       i = 1 2 3 4 5 6 7 8 9 ……
       A = a b a b a b
a a b a b …
       B =        a b a b
a c b
       j =        1 2 3 4
5 6 7

       由于P[5]=3,因此新的j=3:

       i = 1 2 3 4 5 6 7 8 9 ……
       A = a b a b a b
a a b a b …
       B =            a b
a b a c b
       j =            1 2
3 4 5 6 7

       这时,新的j=3仍然不能满足A[i+1]=B[j+1],此时我们再次减小j值,将j再次更新为P[3]:

       i = 1 2 3 4 5 6 7 8 9 ……
       A = a b a b a b
a a b a b …
       B =             
a b a b a c b
       j =             
1 2 3 4 5 6 7

       现在,i还是7,j已经变成1了。而此时A[8]居然仍然不等于B[j+1]。这样,j必须减小到P[1],即0:

       i = 1 2 3 4 5 6 7 8 9 ……
       A = a b a b a b
a a b a b …
       B =                  a b a b a c b
       j =             
0 1 2 3 4 5 6 7

       终于,A[8]=B[1],i变为8,j为1。事实上,有可能j到了0仍然不能满足A[i+1]=B[j+1](比如A[8]="d"时)。因此,准确的说法是,当j=0了时,我们增加i值但忽略j直到出现A[i]=B[1]为止。
       这个过程的代码很短(真的很短),我们在这里给出:

程序代码 程序代码
j:=0;
for i:=1 to n do
begin
      while (j>0) and (B[j+1]<>A[i]) do j:=P[j];
      if B[j+1]=A[i] then j:=j+1;
      if j=m then
      begin
         writeln('Pattern occurs with shift ',i-m);
         j:=P[j];
      end;
end;



       最后的j:=P[j]是为了让程序继续做下去,因为我们有可能找到多处匹配。
       这个程序或许比想像中的要简单,因为对于i值的不断增加,代码用的是for循环。因此,这个代码可以这样形象地理解:扫描字符串A,并更新可以匹配到B的什么位置。

       现在,我们还遗留了两个重要的问题:一,为什么这个程序是线性的;二,如何快速预处理P数组。
       为什么这个程序是O(n)的?其实,主要的争议在于,while循环使得执行次数出现了不确定因素。我们将用到时间复杂度的摊还分析中的主要策略,简单地说就是通过观察某一个变量或函数值的变化来对零散的、杂乱的、不规则的执行次数进行累计。KMP的时间复杂度分析可谓摊还分析的典型。我们从上述程序的j 值入手。每一次执行while循环都会使j减小(但不能减成负的),而另外的改变j值的地方只有第五行。每次执行了这一行,j都只能加1;因此,整个过程中j最多加了n个1。于是,j最多只有n次减小的机会(j值减小的次数当然不能超过n,因为j永远是非负整数)。这告诉我们,while循环总共最多执行了n次。按照摊还分析的说法,平摊到每次for循环中后,一次for循环的复杂度为O(1)。整个过程显然是O(n)的。这样的分析对于后面P数组预处理的过程同样有效,同样可以得到预处理过程的复杂度为O(m)。
       预处理不需要按照P的定义写成O(m^2)甚至O(m^3)的。我们可以通过P[1],P[2],...,P[j-1]的值来获得P[j]的值。对于刚才的B="ababacb",假如我们已经求出了P[1],P[2],P[3]和P[4],看看我们应该怎么求出P[5]和P[6]。P[4]=2,那么P [5]显然等于P[4]+1,因为由P[4]可以知道,B[1,2]已经和B[3,4]相等了,现在又有B[3]=B[5],所以P[5]可以由P[4] 后面加一个字符得到。P[6]也等于P[5]+1吗?显然不是,因为B[ P[5]+1 ]<>B[6]。那么,我们要考虑“退一步”了。我们考虑P[6]是否有可能由P[5]的情况所包含的子串得到,即是否P[6]=P[ P[5] ]+1。这里想不通的话可以仔细看一下:

           1 2 3 4 5 6 7
       B = a b a b a c b
       P = 0 0 1 2 3 ?

       P[5]=3是因为B[1..3]和B[3..5]都是"aba";而P[3]=1则告诉我们,B[1]和B[5]都是"a"。既然P[6]不能由P [5]得到,或许可以由P[3]得到(如果B[2]恰好和B[6]相等的话,P[6]就等于P[3]+1了)。显然,P[6]也不能通过P[3]得到,因为B[2]<>B[6]。事实上,这样一直推到P[1]也不行,最后,我们得到,P[6]=0。
       怎么这个预处理过程跟前面的KMP主程序这么像呢?其实,KMP的预处理本身就是一个B串“自我匹配”的过程。它的代码和上面的代码神似:

程序代码 程序代码
P[1]:=0;
j:=0;
for i:=2 to m do
begin
      while (j>0) and (P[j+1]<>P[i]) do j:=P[j];
      if P[j+1]=P[i] then j:=j+1;
      P[i]:=j;
end;



       最后补充一点:由于KMP算法只预处理B串,因此这种算法很适合这样的问题:给定一个B串和一群不同的A串,问B是哪些A串的子串。

       串匹配是一个很有研究价值的问题。事实上,我们还有后缀树,自动机等很多方法,这些算法都巧妙地运用了预处理,从而可以在线性的时间里解决字符串的匹配。我们以后来说。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值