字符串匹配KMP算法详解

这篇文章主要是解释KMP算法的原理,KMP算法是BF(Brute Force)算法的一种改进算法,什么是BF算法这里不多做解释。

 

1.KMP算法实现思路:

  每当一趟匹配过程中出现字符比较不等时,不需要回溯主串上面的指针i而是利用已经计算出的模式串P在j位置前面的子串P0…Pj-1部分匹配值k将模式向右滑j-k个字符,然后继续进行比较。  

2.什么是部分匹配值:

  首先这里要引入”前缀”和”后缀”的概念,

  (1)前缀:指除了最后一个字符以外,一个字符串的全部头部组合;

  (2)后缀:指除了第一个字符以外,一个字符串的全部尾部组合;

  部分匹配值:就是”前缀”和”后缀”的最长的共有元素的长度,如以字符串”ABCDABD”为例:

  -   “A”的前缀和后缀都为空集,共有元素的长度为0;

  - ”AB”的前缀为[A],后缀为[B],共有元素的长度为0;

  - ”ABC”的前缀为[A, AB],后缀为[BC, C],共有元素的长度0;

  - ”ABCD”的前缀为[A, AB, ABC],后缀为[BCD, CD, D],共有元素的长度为0;

  - ”ABCDA”的前缀为[A, AB, ABC, ABCD],后缀为[BCDA, CDA, DA, A],共有元素为”A”,长度为1;

  - ”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,长度为2;

  - ”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB],后缀为[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的长度为0。

 

3.下面来看证明过程:
 
假设主串S的长度为n,模式串P的长度为m,i为主串S当前位置的指针,j为模式串P当前位置的指针:
  S0…..Si-jSi-j+1Si-j+2…….Si-2Si-1………..Sn
        P0 P1 P2……………Pj-2Pj-1 

即:Si-jSi-j+1Si-j+2…Si-1=P0 P1 P2…Pj-2Pj-1                      (1-1)

当Si!=Pji不动,模式串P向右移动多少个字符最正确(即要保证不会漏掉可能的匹配或不会重复不必要的匹配过程)

如果P本身的每一个字符都不相同,那么就可以直接将模式串P向右移动j个字符,道理很简单因为P0!=P1!=P2…!=Pj-1,由上面等式(1-1)可知P0也不等于Si-jSi-j+1Si-j+2…….Si-2Si-1中的任何一个(P0 P1 P2…Pj-2Pj-1的部分匹配值为0),所以可以直接从P0开始和Si进行下一轮比较(指针i不需要回溯,指针j回溯到模式串的起始位置)。

但是如果模式串P存在很多重复的字符如:abcabcabd这种情况时就不需要直接将j指针移动到P0了,例如主串为fffffabcabcabcabcabdfffff时

               i       

         fffffabcabcabcabcabdfffff

          abcabcabd

              j

              ↑ 发现 c != d 即 S!= Pj

此时应该怎么移动呢?如果直接将j移动到P0然后和Si比较则会出现漏掉匹配的情况即匹配结束后找不到匹配串,正确的做法是将j—>P5位置(相当于模式串向右滑动3个位置)然后和Si继续比较,如下所示:

               i       

         fffffabcabcabcabcabdfffff

               abcabcabd

              j

为什么是可以直接将模式串向右滑动3个位置呢?这个3是怎么来的?这个就是整个算法的关键点,理解了这一点也就理解了KMP算法的本质。

其实这个3就是根据子串P0 P1 P2…Pj-2Pj-1的部分匹配值k=5求出来的:j-k=8-5=3(j=8,k=5)

根据上面字符串部分匹配值的定义可知当j=8时P0P1…Pj-1等于字符串abcabcab,该字符串的前缀和后缀的最长共有元素的长度为5,即abcabca和bcabcab重叠的部分最大长度为5。

那么这是什么原理呢?为什么P0P1…Pj-1的部分匹配值就是模式P在位置j失配时重新开始匹配的位置呢?为什么不需要回溯i指针及完全回溯j指针到P0,却不会出现漏掉匹配或者怎么能确保这种情况下是没有进行不必要的重复匹配呢?

下面去看分析:

当在j位置失配时有 P!= Si 且等式 Si-jSi-j+1Si-j+2…Si-1=P0 P1 P2…Pj-2Pj-1 必定成立

又由字符串部分匹配值的定义可知P0P1…Pk-1=Pj-kPj-k+1…Pj-1,上面的列子中即P0P1P2P3P4=P3P4P5P6P7(j=8,k=5)

由Pj-kPj-k+1…Pj-1=Si-kSi-k+1…Si-1 可知 P0P1Pk-1=Si-kSi-k+1Si-1;所以在模式串中从P0到Pk-1之间的字符是不需要重复匹配的。因为一定会匹配成功。

前缀和后缀的最长共有元素的意思就是说不可能存在一个y,且y>k使得P0P1P2…Py-1=PjyPjy+1…Pj-1成立(这里是关键,P0P1P2…Py-1就是P串的某一个前缀,PjyPjy+1…Pj-1是P串的某一个后缀,k是该字符串的部分匹配值,所以不可能存在一个y>k使得等式成立),只有当y<=k时等式才会成立;这样既避免了不必要的匹配也不会漏掉可能的匹配结果。

由部分匹配值的定义可以知道:P0P1P2…Pj-1 != P1P2…Pj,P0P1P2Pj-2 != P2P3P4…Pj一直到 P0P1P2…Pj-k+1 != Pj-k-1Pj-kPj-k+1…Pj 

直到j-k次后才会匹配成功P0P1P2…Pk = PjkPjk+1Pjk+2…Pj;这就是KMP算法中当失配时直接将模式串P向右滑动j-k个字符的原理。

模式串P的部分匹配值表怎么求,下篇博文里面再详细说明,其实关键点还是前缀和后缀以及部分匹配值的问题,把这个搞懂了就都懂了。

 

4.实现代码:

 1 public static int kmp(String source,String p){
 2         int[] next = getNext(p);
 3         int i=0,j=0;
 4         while(i<source.length()&&j<p.length()){
 5             if(source.charAt(i)==p.charAt(j)){
 6                 i++;
 7                 j++;
 8             }else if(j==0){
 9                 i++;
10             }else{
11                 j = next[j-1];    
12             }
13         }
14         if(j>=p.length())
15             return i-j;
16         return -1;
17     }
18     
19     /**
20      * Acquire pattern string p's partial match table
21      */
22     public static int[] getNext(String p){
23         int[] next = new int[p.length()];
24         int i=1,j=0;
25         next[0] = 0;
26         while(i<p.length()-1){
27             while(j>0&&p.charAt(i)!=p.charAt(j))
28                 j = next[j-1];
29             if(p.charAt(i)==p.charAt(j))
30                 j++;
31             next[i++] = j;
32         }
33         return next;
34     }

 

 

    原文作者:JAVA Miner
    原文地址: https://www.cnblogs.com/javaminer/p/3420213.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞