浅谈KMP算法及实现

题目描述

这里通过lintcode上的字符串查找这道题,引入字符串匹配问题。

对于一个给定的 source 字符串和一个 target 字符串,你应该在 source 字符串中找
出 target 字符串出现的第一个位置(从0开始)。如果不存在,则返回 -1。

样例
如果 source = "source" 和 target = "target",返回 -1。
如果 source = "abcdabcdefg" 和 target = "bcd",返回 1。

BF匹配算法(蛮力匹配算法)

算法思想

从主串S的第 pos 开始,和模式串T的第一个字符进行比较,若相等,则继续逐个比较后续的字符;
否则回溯到主串S的第 pos+1 字符位置处重新与模式串T进行比较。
直到模式串T中的每一个字符依次与主串S的一个连续字符序列完全相同,则称匹配成功,此时
返回模式字符串T的第一个字符在主串S中的位置;否则匹配失败,返回-1。

时间复杂度

n,m分别是主串和模式串的长度
最坏情况下:O(n*m)    

代码实现

class Solution {
    public int strStr(String source, String target) {
        if(source==null || target==null)
            return -1;

        //这里规定下标从1开始,当然也可以从0开始
        int i=1,j=1;
        int slen=source.length();
        int tlen=target.length();

        if(tlen==0)
            return 0;
        if(slen==0 || slen<tlen)
            return -1;

        while(i<=slen && j<=tlen){
            if(source.charAt(i-1)==target.charAt(j-1)){
                i++;
                j++;
            }else{
                //计算匹配失败后回溯到的主串S位置(pos+1)
                //下标从0开始为: i=i-j+1;
                i=i-j+2;
                j=1;
            }
        }
        //匹配成功,返回出现下标
        //为满足题目输出要求,下标从0开始。
        if(j>tlen)      return i-tlen-1;    
        else            return -1;
    }
}

KMP匹配算法

算法思想

与BF算法相比,KMP算法消除了主串S匹配失败时的指针回溯。

KMP算法当匹配失败时,主串S中的i指针不需回溯,而是根据已经得到的部分匹配结果将模式串尽可能远的向右滑动,然后继续进行比较。

匹配过程

假设主串S为: acabaabaabcacaabc
模式串为:abaabcac

这里需要使用到一个next数组(后面会提到,现在只需理解匹配过程)

j12345678
next[j]01122312
(1)第一次匹配

i   1  2    
    a  c  a  b  a  a  b  a  a  b  c  a  c  a  a  b  c
    a  b  a  a  b  c  a  c
       ^
j   1  2

当i=2时主串与j=2时模式串不匹配,查表next[2]=1;则需要将模式串中第一个字符与i=2位置的字符进行匹配,即模式串后移一位。

(2)第二次匹配

i   1  2    
    a  c  a  b  a  a  b  a  a  b  c  a  c  a  a  b  c
       a  b  a  a  b  c  a  c
       ^
j      1

next[1]=0,此时需要将主串和模式串都向后移动一位(此时j=0,移动一位即是模式串第一个字符),即从i=3与模式串T1重新比较

(3)第三次匹配   

i   1  2  3      ...     8  
    a  c  a  b  a  a  b  a  a  b  c  a  c  a  a  b  c
          a  b  a  a  b  c  a  c
                         ^
j         1      ...     6

next[6]=3,则需要将模式串中第3个字符与i=8位置的字符进行匹配,即模式串后移3位。

(4)第四次匹配

i   1  2  3      ...     8       ...       14   
    a  c  a  b  a  a  b  a  a  b  c  a  c  a  a  b  c
                   a  b  a  a  b  c  a  c
                         ^
                j  1  2  3       ...       9

关于next数组

next[j]表明当模式串中第j个字符与主串中相应的字符不相等时,在模式串中需要重新和主串中该字符进行比较的字符位置。

计算next数组

当next函数中定义的集合不为空时,next[j]的值等于串"T[1]T[2]...T[j-1]"的
真前缀子串和真后缀子串相等时的最大子串长度+1。

那么什么是真前(后)缀子串呢:就是不包含自身的前(后)缀子串。
如:aba
真前缀子串: a  ab
真后缀子串: a  ba

当j=1时,串不存在,next[1]=0;
当j=2时,规定next[2]=1;
要求next[j+1],串为"T[1]T[2]...T[j]",要找该串的真前缀子串等于该串的真后缀子串,
即只需比较 T[j] 和 T[k] 是否相等(k=next[j]):
如相等,next[j+1]=next[j]+1;
否则,继续比较 T[j] 和 T[k'] 是否相等(k'=next[k])?
   如相等,next[j+1]=next[k]+1;
   否则,继续比较 T[j] 和 T[ next[k'] ] 是否相等?
   ...
   如果直到 next[k*]=0 都不相等,则 next[j+1]=1; 

算法时间复杂度

n,m分别是主串和模式串的长度
时间复杂度为:O(n+m)

代码实现

class Solution {
    //求next数组的过程
    public static int[] getNext(String str){
        int len=str.length();
        int[] next=new int[len+1];
        next[1]=0;
        int j=1,k=0;
        while(j<len){
            //要计算next[j+1]:比较T[j]和T[next[j]]
            if(k==0 || str.charAt(j-1)==str.charAt(k-1)){
                ++j;
                ++k;
                next[j]=k;
            }
            else k=next[k];
        }
        return next;
    }

    public static int strStr(String source, String target) {
        if(source==null || target==null)
            return -1;

        int slen=source.length();
        int tlen=target.length();

        if(tlen==0)
            return 0;
        if(slen==0 || slen<tlen)
            return -1;

        int[] next=getNext(target);
        int i=1,j=1;
        while(i<=slen && j<=tlen){
            if(j==0 || source.charAt(i-1)==target.charAt(j-1)){
                i++;
                j++;
            }
            else j=next[j];
        }
        if(j>tlen)      return i-tlen-1;
        else            return -1;
    }

}

KMP代码的优化

举个栗子

试着考虑这个问题,如果主串为"aaabaaaab",模式串为"aaaab"?

模式串对应的next函数值如下:

j12345
next[j]01234

这个效率有点低,所以我们引入 nextval 数组:

其计算方式如下:
nextval[1]=0;
如计算nextval[j],则比较 T[j] 与 T[k](k=next[j])?
如相等:  nextval[j]=nextval[k];
否则:    nextval[j]=next[j];      
j12345
next[j]01234
nextval[j]00004

代码如下

class Solution {
    public static int[] getNext(String str){
        int len=str.length();
        int[] next=new int[len+1];
        next[1]=0;
        int j=1,k=0;
        while(j<len){
            if(k==0 || str.charAt(j-1)==str.charAt(k-1)){
                ++j;
                ++k;
                next[j]=k;
            }
            else k=next[k];
        }
        return next;
    }
    //这里根据得到的next数组来计算nextval数组
    public static int[] getNextVal(String str){
        int len=str.length();
        int[] nextval=new int[len+1];

        int j=2,k=0;
        nextval[1]=0;
        int[] next=getNext(str);
        while(j<=len){
            k=next[j];
            if(str.charAt(j-1)==str.charAt(k-1))
                nextval[j]=nextval[k];
            else
                nextval[j]=next[j];
            j++;
        }
        return nextval;
    }

    public static int strStr(String source, String target) {
        if(source==null || target==null)
            return -1;

        int slen=source.length();
        int tlen=target.length();

        if(tlen==0)
            return 0;
        if(slen==0 || slen<tlen)
            return -1;

        int[] nextval=getNextVal(target);
        int i=1,j=1;
        while(i<=slen && j<=tlen){
            if(j==0 || source.charAt(i-1)==target.charAt(j-1)){
                i++;
                j++;
            }
            else j=nextval[j];
        }
        if(j>tlen)      return i-tlen-1;
        else            return -1;
    }

}

两种算法的比较

BF算法时间复杂度为O(n*m),但实际执行近似与O(n+m),因此仍被使用。
KMP算法仅当模式串与主串之间存在许多部分匹配情况下,才会比BF算法快。
点赞