认真学习并实现KMP

学习链接:http://blog.csdn.net/v_july_v/article/details/7041827

问题描述:有一个文本串S和一个模式串P,现在要查找P在S中的位置,如何查找?

1、暴力匹配

  当前文本串S匹配到i位置,模式串匹配的j位置,则有:

    a、如果当前字符匹配成功(S[i]==P[j]),则i++,j++,继续匹配下一个字符;

    b、如果当前字符匹配失败(S[i]!=P[j]),则令i=i-(j-1),j=0,相当于,i回溯j-1个字符,j置为0。

  暴力匹配的程序实现如下:

    

 1 public int ViolentMatch() {
 2         while(i<SS.length && j<PP.length)
 3         {
 4             if(SS[i] == PP[j])
 5                 //如果当前字符匹配成功
 6             {
 7                 i++;
 8                 j++;
 9             }
10             else
11             {
12                 //如果匹配不成功,让i回到原来位置+1,j从0开始
13                 i = i-j+1;
14                 j = 0;
15             }
16         }
17         if(j==PP.length)
18         {
19             //匹配成功,返回模式串P在文本串S中的位置,否则返回-1
20             return i-j;
21         }
22         else
23         {
24             return -1;
25         }
26     } 

  例如,S= “BBCABCDAB ABCDABCDABDE” 和P=“ABCDABD”,现在要拿模式串P去跟文本串匹配,过程如下(图片中S串BBC后的空格是没有的):

    1、S[0]=B,P[0]=A,不匹配。执行b:"如果当前字符匹配失败(S[i]!=P[j]),则令i=i-j+1,j=0,相当于,i回溯j个字符,然后从下一个开始,j置为0,从第      一个开始"。匹配S[1]和P[0],相当于模式串向右移动一位。【i=1,j=0】

      

    2、S[1]!=P[0],还是不匹配,继续执行b,匹配S[2]和P[0],模式串向右移动一位。【i=2,j=0】

      

       执行b,直到【i=4,j=0】。

    3、S[4]=P[0],匹配成功,执行a:(i++,j++),匹配S[5]和P[1]。【i=5,j=1】

        

    4、S[5]=P[1],匹配成功,执行a,匹配S[6]和P[2]。【i=6,j=2】

        

      执行a,直到【i=10,j=6】。

    5、S[10] 为空格,S[10]!=P[6],匹配失败,执行b:(i=i-j+1,j=0),相当于i回溯的原来位置,j重新开始,匹配S[5]和P[0]。【i=5,j=0】

        

    6、S[5]!=P[0] ,匹配失败。至此我们看到,按照暴力匹配的思路,尽管文本串和模式串已经匹配到S[9]和P[5],但是因为最后一个字符不匹配,i得回溯的      开始的位置S[5],j得从0开始P[0]。

        

    而,S[5]和P[0]必然是不匹配的,为什么?

      因为在之前的4步中,我们已经知道S[5]=P[1]=B,而P[0]=A!=p[1],所以,S[5]!=P[0],所以回溯回去必然不匹配。那么有没有一种      算法能够让i保持不回溯,一直向前移动呢?

    答案是肯定的,这就是KMP算法的基本思路。它利用之前已经部分匹配的这个有效信息,保持i不回溯,通过修改j的位置,让模式串尽量的移动到    有效位置。

2、KMP算法

    直接上算法流程,

    当前S串匹配到i位置,P串匹配到j位置:

      如果j=-1,或者当前字符匹配成功(S[i]==P[i]),就令i++,j++,继续匹配下一个字符。

      如果j!=-1并且当前字符匹配失败(S[i]==P[i]),就令i不变,j=next[j]。此举意味着,模式串P相对于文本串S向右移动了j-next[j]个位置。

        换言之,当匹配失败时,模式串P向右移动的位置数:失配字符所在的位置-失配字符对应的next值(next值有一个计算方法在后面),即移动的实        际位数是:j-next[j],且此值大于等于1。

      很快就能意识到next数组各值的含义:代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如,如果next[j]=k,代表j之前的字符串中有最      大长度为K的相同前缀后缀。这也意味着在某个字符失配时,该字符对应的next值会告诉你下一步匹配中,模式串应该调到哪个位置(调到next[j]        处)。如果next[j]=0或-1,则调到模式串开头字符,若next[j]=k且k>0,代表下一次匹配调到j之前的某个字符,而不是调到开头,而具体是从开头      跳过了K个字符。

    步骤:

      1、寻找前缀后缀最长公共元素长度

        对于Pj=P0P1...Pj-1,寻找模式串Pj中长度最大且相等的前缀和后缀

          即寻找满足条件的最大的k,使得P0P1...Pk-1 = Pj-kPj-k+1...Pj-2Pj-1。也就是说,k是模式串中各个子串的前缀后缀的公共元素长度

           ,所以求最大的k,就是看某个子串的哪个前缀后缀的公共元素最多。

          举个例子,如果给定的模式串为“abaabcaba”,那么它的各个子串的前缀后缀的公共元素的最大长度值如下表格所示:

模式串 a b a a b c a b a
k (长度) 0 0 1 1 2 0 1 2 3

      2、求next数组

        根据上一步中求得的各个前缀后缀的公共元素的最大长度求next数组,相当于前者右移一位且初值为-1,如下表所示:

模式串 a b a a b c a b a
next数组 -1 0 0 1 1 2 0 1 2

      3、匹配失败

        模式串向右移动的位数为:j-next[j]。换言之,当模式串的后缀Pj-kPj-K+1...Pj-1跟文本串Si-kSi-k+1...Si-1匹配成功,但Pj和Si匹配失败时,        因为P0P1...Pk-1=Pj-kPj-k+1...Pj-1(next[j]=k),故令j=next[j],从而让模式串右移动j-next[j]位,使得模式串的P0P1...Pk-1对应着文本串        Si-kSi-k+1...Si-1,而后让Pk跟Si继续匹配。

          注意:j是模式串中失配字符的位置,且j从0开始计数。

  综上,KMP的next数组相当于告诉我们:当模式串的某个字符跟文本串中的某个字符匹配失败时,模式串下一步应该调到哪个位置。如果模式串中在j处字符跟文  本串中第i处的字符失配时,下一步用next[j]处的字符继续跟文本串i处的字符匹配,相当于模式串向右移动j-next[j]个位置。 

  下面对上述3个步骤解释:

      1、寻找最长前缀后缀

        如果给定的模式串是“ABCDABD”,从左至右遍历整个模式串,其各个子串的前缀后缀分别如下表所示:

各个子串 前缀 后缀 最大公共长度
A 0
AB A B 0
ABC A,AB C,BC 0
ABCD A,AB,ABC D,CD,BCD 0
ABCDA A,AB,ABC,ABCD A,DA,CDA,BCDA 1
ABCDAB A,AB,ABC,ABCD,ABCDA B,AB,DAB,CDAB,BCDAB 2
ABCDABD A,AB,ABC,ABCD,ABCDA,ABCDAB D,BD,ABD,DABD,CDABD,BCABD 0

         也就是,原字符串对应的各个前缀后缀的公共元素最大长度为:下表(后面称为最大长度表):

字符 A B C D A B D
最大公共长度 0 0 0 0 1 2 0

        基于最大长度表匹配:

           因为模式串中首尾可能有重复的字符,故可以得到如下结论:

            失配时,模式串向右移动的位数为:已经匹配字符数-失配字符上一位字符所对应的最大长度值

           下面就结合这个结论和最大长度表进行字符串匹配。如果给定文本串为“BBC ABCDABABCDABCDABDE”和模式串“ABCDABD”,现在要           拿模式串跟文本串匹配,如下图所示:

            

            a、因为模式串中的字符A跟文本串中的字符B、B、C、空格一开始就不匹配,所以不必考虑结论,直接将模式串不断的右移一位即可,直              到模式串中的A和文本串中的第5个字符A匹配成功:

              

            b、继续往后匹配,当模式串中最后一个字符D跟文本字符串空格失配时,显而易见,模式串需要向右移动。但向右移动多少位呢?因为此              时已经匹配的字符数为6个(ABCDAB),然后根据最大长度表可得失配字符D的上一位字符B对应的长度值为2,所以根据上述结论,              向右移动6-2=4位。

                

            C、模式串向右移动4位后,发现模式串的C与空格再度失配,因为此时已经匹配了两个字符(AB),且上一位字符B对应的长度值为0,所              以向右移动2-0=2位。

              

            D、移动两位后A与空格失配,向右移动1位。(因为初始值next[0]=-1)

              

            E、继续比较,发现D与C失配,故向右移动的位数为:已匹配的字符数6减去上一位字符B对应的长度为2,即向右移动6-2=4位。

              

            F、经历过E步骤后,发现匹配成功,匹配结束。

              

      通过上述步骤过程可以看出,问题的关键就是寻找模式串中最大长度的相同前缀和后缀,找到了模式串中每个字符之前的的前缀和后缀公共部分的最大长      度后,便  可以基于此进行匹配。这个最大长度表就是nex的数组要表达的意义。

      2、根据最大长度表求解next数组

        由上文,我们已经知道,字符串“ABCDABD”各个前缀后缀的最大公共元素长度分别为上文中的表格所示

字符 A B C D A B D
最大公共长度 0 0 0 0 1 2 0

        而且根据这个表格可以得到结论:

            失配时,模式串向右移动的位数为:已经匹配字符数-失配字符上一位字符所对应的最大长度值

        利用这个表和结论进行匹配时,我们发现,当发现一个字符失配时我们没有必要考虑当前这个失配字符,因为我们每次失配时都是关注失配字符上一        个字符对应的最大长度值。如此,便引出了next数组。

        给定字符串“ABCDABD”,可以求得它的next数组如下:

字符 A B C D A B D
next -1 0 0 0 0 1 2

        和最大长度表对比发现,next数组相当于将最大长度表向右移动了一位,初始值赋值为-1。根据最大长度表求出next数组后,从而有

            失配时,模式串向右移动的位数为:失配字符的位置-失配字符对应的next值

        而,无论是最大长度表还是基于next数组匹配,两者得到的结果是一样的。

   基于上面的理解,如何计算next数组,可以采用递推。

    1、如果对于值k,已有P0P1...Pk-1 = Pj-kPj-k+1...Pj-1,相当于next[j]=k。

      此意味着什么呢?究其本质,next[j]=k代表P[j]之前的模式子串中,有长度为k的相同前缀和后缀。有了这个next数组,在KMP匹配中,当模式串后缀      中的j处的字符失配时,模式串向右移动j-next[j]位。

    2、关键的问题是,如果知道next[0...j],如何求出next[j+1]呢?

      对于P的前j+1个序列字符:

      若P[k]==P[j],则next[j+1]=next[j]+1=k+1;

      若P[k]!=P[j],如果此时P[next[k]]==P[j],则next[j+1]=next[k]+1,否则继续递归重复此过程。

      相当于在字符P[j+1]之前不存在长度为k+1的前缀“P0P1...P[k]”和“P[j-k]P[j-k+1]...P[j]”相等,那么是否存在另一个值t+1<k+1,使得长度更小的前      缀“P0P1...P[t]”和后缀“P[j-t]P[j-t+1]...P[j]”呢?如果存在,那么这个t+1便是Next[j+1]的值,此相当于next数组进行P串前串和后缀的匹配。

      至此,可能仍然不好理解求解next[]数组的原理,再进一步说明一下:

      如下图示:假定给定的模式串为ABCDABCE,且已知next[j]=k(例如P0...Pk-1=Pj-k...Pj-1=AB,则k=2),现要求next[j+1]等于多少?因为         P[k]=P[j]=C,所以next[j+1]=next[j]+1=k+1(可以看出next[j+1]=3),代表字符E的模式串中,右长度k+1的相同前缀后缀。

模式串 A B C D A C C E
前后缀同长 0 0 0 0 1 2 3 0
next值 -1 0 0 0 0 1 2
索引 P0 P1 P2(k) P3 P4 P5 P6(j) P7(J+1)

      如果Pk!=Pj呢?说明“P0Pk-1Pk”!="Pj-kPj-1Pj",换言之,当Pk!=Pj后,字符E前右多大长度的相同前缀后缀呢?如下图,很明显,因为C不同于D,      所以ABC和ABD不同,即字符E前没有k+1的相同前缀后缀。也就不能简单的令next[j+1]=next[j]+1,所以只能寻找长度更短的相同前缀后缀。

模式串 A B C D A B D E
前后缀同长 0 0 0 0 1 2 0 0
next值 -1 0 0 0 0 1 2 ?
索引 P0 Pk-1 Pk Pk+1 Pj-k Pj-1 Pj Pj+1

      结合上图来讲,若能在前缀“P0Pk-1Pk”中不断的递归k=next[k],找到一个字符Pk‘也为D,代表Pk'=Pj,且满足P0Pk'-1Pk'=Pj-k'Pj-1Pj,则最大相同      的前缀后缀长度为k'+1,从而next[j+1]=k'+1。否则前缀中没有D,则E的next值为0。

      综上,可以通过递推求得next数则,程序如下:    

public void getNext()
    {
        next[0] = -1;
        int k = -1;
        int j = 0;
        while(j<PP.length-1)
        {
            //
            if(k!=-1 && PP[j]!=PP[k])
            {
                k = next[k];
            }
            else {
                ++k;
                ++j;
                next[j] = k;    
            }
            /*
            if(k==1 || PP[j]==PP[k])
            {
                ++k;
                ++j;
                next[j] = k;
            }
            else {
                 k = next[k];
            }
            */
        }        
    } 

      基于next数组匹配:

         下面基于next数组对最开始的例子匹配:文本串:BBC ABCDAB ABCDABCDABDE和模式串:ABCDABD。模式串的next数组如下:

字符 A B C D A B D
next -1 0 0 0 0 1 2

        匹配之前,再来回顾一下KMP算法流程:

        假设现在文本串S匹配到 i 位置,模式串P匹配到 j 位置:

          如果j=-1或者当前字符匹配成功,令i++,j++,继续匹配下一个字符。

          如果j!=-1,且当前字符匹配失败,令i不变,j=next[j]。此举意味着模式串向右移动j-next[j]个位置,且此值大于等于1。

        KMP程序实现:        

public int KmpSearch() {
        while(i<SS.length && j<PP.length)
        {
            if(j==-1 || SS[i] == PP[j])
            {
                //如果当前字符匹配成功
                i++;
                j++;
            }
            else
            {
                //如果j不等于-1并且Si与Pj不匹配,则令i不变,j=next[j],模式串向右移动j-next[j]
                j = next[j];
            }
        }
        if(j==PP.length)
        {
            return i-j;
        }
        else
        {
            return -1;
        }
    }

        匹配过程和基于最大长度表的匹配过程完全一样,这也侧面说明基于next数组的匹配和基于最大长度表的匹配是等价的。

next数组和有限状态自动机

   next负责把模式串向前移动,且当第j位不匹配时,用next[j]位和主串匹配,就像打了一张表,此外,next也可以看做有限状态自动机的状态,在已经读了多少  字符的情况下,失配后,前面读的若干字符是有用的。

  

next数组的优化

  比如,如果用之前的next的数组方法求模式串abab的next数组,可以得到其next数组为-1 0 0 1(0 0 1 2整体向右移动一位,初值赋值为-1),当它跟下图中的文本串匹配时,发现b与c失配,于是模式串右移j-next[j]=3-1=2位。

  

  右移2位后,b又跟c失配。事实上,因为在上一步的匹配中,已经得知P[3]=b,与S[3]=c失配,而右移动两位之后,让P[next[3]]=P[1]=b再跟 S[3]匹配,必然失配。问题在哪呢?

  

  问题出在不该出现P[j]=P[next[j]]这个赋值上。因为: 当S[i]!=P[j]时,下一步匹配必然是S[i]和P[next[j]]进行匹配,所以不能允许P[j] = P[next[j]]。

  P[j]与S[i]已经失配,再用跟P[j]相等的字符与S[i]匹配,显然,必然失配。

  因此,求解next数组的代码更改如下:

  

  利用优化过后的next数组,可知模式串abab的新next数组为:-1 0 0 1。

  原始next 数组是前缀后缀最长公共元素长度值右移一位, 然后初值赋为-1而得,那么优化后的next 数组如何快速心算出呢?实际上,只要求出了原始next 数  组,那么可根据原始next 数组快速求出优化后的next 数组。还是以abab为例,如下表格所示:

    

  基于优化过后的next数组做匹配,能使得匹配过程更快。

KMP的时间复杂度

  理解KMP十分容易,把握如下几点十分重要:

  1、如果模式串中存在相同的前缀和后缀,即Pj-kPj-k+1...Pj-1 = P0P1...Pk-1,那么Pj与Si失配后,让模式串的前缀移动至后缀的位置,让Si和Pk继续匹配。

  2、相当于将模式串向右移动了j-k位。

  3、因为k值是变化的,所以用next[j]表示j处字符失配后,下一次模式串应该调到的位置。

  4、而next[j]应该等于多少呢?next[j]的值由j之前的模式串子串中有多大长度的相同前缀后缀所决定,最大长度为k,则next[j]=k。

  接下来,分析KMP的算法流程:

    算法最坏的情况是,当模式串的首字符位于i-j的位置才匹配成功。如果文本串的长度为n,模式串的长度为m,那么匹配过程的时间复杂度为O(n),计算next  数组的时间复杂度为O(m),因此KMP的整体时间复杂度为O(n+M)。

程序代码:

    

package kmp;

public class Kmp {
    String S = "ABCDEFG";
    String P = "EFG";
    
    public String getS() {
        return S;
    }
    public void setS(String s) {
        S = s;
    }
    public String getP() {
        return P;
    }
    public void setP(String p) {
        P = p;
    }
    char [] SS = S.toCharArray();
    char [] PP = P.toCharArray();
    
    int [] next = new int[PP.length];
    
    int i ,j = 0;
    public int ViolentMatch() {
        while(i<SS.length && j<PP.length)
        {
            if(SS[i] == PP[j])
                //如果当前字符匹配成功
            {
                i++;
                j++;
            }
            else
            {
                //如果匹配不成功,让i回到原来位置+1,j从0开始
                i = i-j+1;
                j = 0;
            }
        }
        if(j==PP.length)
        {
            //匹配成功,返回模式串P在文本串S中的位置,否则返回-1
            return i-j;
        }
        else
        {
            return -1;
        }
    }
    public int KmpSearch() {
        while(i<SS.length && j<PP.length)
        {
            if(j==-1 || SS[i] == PP[j])
            {
                //如果当前字符匹配成功
                i++;
                j++;
            }
            else
            {
                //如果j不等于-1并且Si与Pj不匹配,则令i不变,j=next[j],模式串向右移动j-next[j]
                j = next[j];
            }
        }
        if(j==PP.length)
        {
            return i-j;
        }
        else
        {
            return -1;
        }
    }
    public void getNext()
    {
        next[0] = -1;
        int k = -1;
        int j = 0;
        while(j<PP.length-1)
        {
            //
            if(k!=-1 && PP[j]!=PP[k])
            {
                k = next[k];
            }
            else {
                ++k;
                ++j;
                next[j] = k;    
            }
            /*
            if(k==1 || PP[j]==PP[k])
            {
                ++k;
                ++j;
                next[j] = k;
            }
            else {
                 k = next[k];
            }
            */
        }        
    }    

}
package kmp;

public class KMPMain {
    public static void main(String[] args) {
        Kmp kmp = new Kmp();
        //kmp.setP();
        //kmp.setS();
        int i = kmp.ViolentMatch();
        System.out.println("模式串P在文本串S中的位置:"+i);
        kmp.getNext();
        int j = kmp.KmpSearch();
        System.out.println("模式串P在文本串S中的位置:"+j);    
    }

}


 

  

       

原文地址:https://www.cnblogs.com/Jiaoxia/p/3901231.html