跳到正文
Jian-Sparrow 的博客
返回

清晰易懂地介绍KMP算法

一个简单的假设

例子:
正文: …….
模式字符串: B A A A A
其中,正文长度为N,模式字符串长度为M,一般来说N>>M。
我们先来做一个假设,假设正文中仅有A和B两个字符组成。那么如果第五个字符匹配失败,可知正文对应部分肯定为B A A A B。那么此时,我们没有必要去回退文本指针i,因为正文对应部分的第2-4个字符均为A,都和模式字符串的第一个字符B不相匹配。这时我们可以直接将i加1,以比较文本的下一字符和模式字符串中的第二个字符。这样我们最多仅仅会进行N次字符串比较。这样我们最多仅仅会进行N次字符串比较。上述情况是很特殊的,但其思想是值得思考的,那么我们可以将这种思想抽象化以使得其可以适用于所有情况吗?
暴力求解法如图所示:
GitHub

可以看到,上述暴力算法中,很多指针i的移动时没有必要的。为了提升字符串匹配算法的性能,我们将介绍一个经典的且令人印象深刻的算法:Knuth-Morris-Pratt算法。

KMP算法的创新

在介绍KMP算法的具体实现之前,先来介绍KMP算法的创新之处:
当出现不匹配时,就能知晓一部分文本的内容,可以利用这些信息避免讲指针回退到所有这些已知的字符之前。
此算法的通用性令人印象深刻:在匹配失败时从能将模式字符串指针j设置为某个值以使文本指针i不回退

(if the beginning part of a string is appearing again somewehere else in the string then don’t again compare the characters right avoid the comparison and don’t move i back again like it was happening in basic algorithm.)

详细介绍KMP算法

下面,我们会从前缀和后缀的角度来详细讲解KMP算法。

对于一个模式字符串“ abcdabc ”来说,其前缀和后缀分别为:
prefix:a,ab,abc,abcd,abcda,abcdab
suffix:c,bc,abc,dabc,cdabc,bcdabc
对于这个字符串来说,其前缀和后缀中都有“abc”,即“abc”这个子字符串重复出现了。
为了记录这种重复性,生成一个数组。对于这个数组,我们称之为LPS(longest prefix which is same as some suffix)
下面举几个例子来说明LPS中的值怎么根据模式字符串来生成。

  1. a b c d a b e a b f
       0 0 0 0 1 2 0 1 2 0(LPS)
  2. a b c d e a b f a b c
        0 0 0 0 0 1 2 0 1 2 3(LPS)

在得到了LPS数组后,KMP算法的后续工作也就比较简单了。下面我们将从一个例子入手,去介绍剩下的过程。

tex: a b a b c a b c a b a b a b d

pattern: a b a b d
根据模式字符串pattern,我们得到LPS数组为:-1 0 0 1 2 0 (在最前面设为-1是为了编程方便,也可以不加)
设指向tex的指针为i,指向pattern的指针为j。
当i指向5,j指向5的时候,字符不匹配,根据LPS数组来回退j。此时LPS的第j个值为2,所以j回退到2,i不变,继续比较。字符仍不匹配,j回退到0,i不变,继续比较。重复上述过程,即可得到最后的匹配结果。

具体的代码实现:

private int[] getNext(String pattern) {
    int j = 0;
    int k = -1;
    int len = pattern.length();
    int []next = new int[len+1];
    next[0] = -1;

    while (j < len - 1) {
        if (k == -1 || pattern.charAt(k) == pattern.charAt(j)) {
            ++k;
            ++j;
            next[j] = k;
        } else {
            k = next[k];
        }
    }
    return next;
}
private int kmp(String tex, String pattern) {
    int tlen = tex.length();
    int plen = pattern.length();
    int []next = getNext(pattern);
    int i=0,j=0;
    while(i<tlen && j<plen){
        if(tex.charAt(i)==pattern.charAt(j)){
            ++i;
            ++j;
        } else{
            if(next[j]==-1){
                ++i;
                j=0;
            }else {
                j = next[j];
            }
        }
        if(j==plen) return i-j;
    }
    return -1;
}

暴力匹配的代码实现:

int strStr1(String haystack, String needle) {
       char []arrays = haystack.toCharArray();
       char []mapp = needle.toCharArray();
       if(mapp.length==0 && arrays.length==0)
           return 0;
       if(mapp.length==0)
           return 0;
       if(arrays.length==0 || arrays.length<mapp.length)
           return -1;
       int flag = 0;
       for(int i=0;i<arrays.length;i++){
           if(arrays[i]==mapp[0]){
               flag = 0;
               int k = i;
               for(int j=0;j<mapp.length;j++){
                   if(k<arrays.length && arrays[k]==mapp[j] ){
                       ++k;
                       ++flag;
                   }
                   else{
                       break;
                   }
               }
               if(flag == mapp.length){
                   return i;
               }
           }
       }
       return -1;
   }

时间复杂度:
KMP为O(m+n)
暴力匹配为O(mn)
其中,m为tex长度,n为pattern长度。



    上一篇
    小米之家站店总结
    下一篇
    kafka总结