理解KMP算法

字符串模式匹配是一个经常用到的功能,比如当我们在一个文档中Ctrl+F 查找一个字符串时,就用到了字符串模式匹配的知识:其实现算法是怎样的呢?

假设主字符串为S:BBC ABCDAB ABCDABCDABDE

模式字符串为T: ABCDABD

1)朴素模式匹配算法(暴力模式匹配)

在暴力字符串匹配过程中,我们会从S[0] 跟 T[0] 匹配,如果相等则匹配下一个字符,直到出现不相等的情况,此时我们会简单的丢弃前面的匹配信息,然后从S[1] 跟 T[0]匹配,循环进行,直到主串结束,或者出现匹配的情况。这种简单的丢弃前面的匹配信息,造成了极大的浪费和低下的匹配效率。

/*
 * 朴素模式匹配算法
*/
int match(char* s,char* t)
{
    int j=0;
    int i=0;
    int *next=getNext(t);
    while(t[i]!='\0'&&s[j]!='\0')
    {
        if(s[j]==t[i])
        {
            i++;j++;
        }
        else {
            j=j-i;
            i=0;
        }
    }
    if(t[i]=='\0')
    { return j-i;}
    else
    { return -1;}
}

2)KMP模式匹配算法

在KMP算法中,对于每一个模式串我们会事先计算出模式串的内部匹配信息,在匹配失败时最大的移动模式串,以减少匹配次数。

比如,在简单的一次匹配失败后,我们会想将模式串尽量的右移和主串进行匹配。右移的距离在KMP算法中是如此计算的:
 
以下是从一篇博客(http://www.cnblogs.com/c-cloud/p/3224788.html)中看到的我认为是关于KMP算法最好理解的一种说法:
 
1、
 首先,字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
 
2、
 因为B与A不匹配,搜索词再往后移一位。
 
3、
 就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
 
 
 4、
 
 接着比较字符串和搜索词的下一个字符,还是相同。
 
5、
直到字符串有一个字符,与搜索词对应的字符不相同为止。
 
6、
这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把"搜索位置"移到已经比较过的位置,重比一遍。
 
7、
一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是"ABCDAB"。KMP算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,继续把它向后移,这样就提高了效率。
 
8、
怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。
 
   9、

已知空格与D不匹配时,前面六个字符"ABCDAB"是匹配的,个数为6个。查表可知,字符D对应的部分匹配值"为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 - 对应的部分匹配值

因为 6 - 2 等于4,所以将搜索词向后移动4位。

 

10、

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2("AB"),C 对应的"部分匹配值"为0。所以,移动位数 = 2 - 0,结果为 2,于是将搜索词向后移2位。

 

11、

因为空格与A不匹配,已匹配的字符数为0,A对应的"部分匹配值"为-1,移动位数=0-(-1),结果为1,继续后移一位,,这也解释了第1,2步骤中往后移动1位的原因。

12、

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 - 2,继续将搜索词向后移动4位。

13、

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 - 0,再将搜索词向后移动7位,这里就不再重复了。

下面介绍《部分匹配表》是如何产生的。

 

     我们的移动位数=已经匹配的字符数-当前不匹配的字符对应的"部分匹配值';
 
 
索引 0 1 2 3 4 5 6
搜索词 A B C A B D
next[j] -1 0 0 0 0 1 2
移动的位数 1 1 2 3 4 4 4
下面来计算next[j]的计算:
以下内容节选自博客:http://youlvconglin.blog.163.com/blog/static/5232042010530101020857/ 部分有删改:

 

next函数的过程是一个递推的过程:

      1.首先由定义得next[0]=-1next[1]=0;

      2.假设已知next[j]=k,又T[j] = T[k],则显然有next[j+1]=k+1

      3.如果T[j]!= T[k],则令k=next[k],直至T[j]等于T[k]为止。

注:

1.虽然next定义中没有明确指出next[1]=0,但由0<k<j的条件很容易判断出next[1]只能等于0;

2.next[j]=k表明在T串中的字符T[k]之前存在一个长度最大的子串"tj-ktj-k+1…tj-1"和T串中的子串 "t0t1…tk-1" 相等,而现在又知道了tj=tk,这就是说,在字符T[k+1]之前存在着一个长度最大的子串使得等式 "t0t1…tk"="tj-ktj-k+1…tj"成立,则根据next函数值的定义不就得到next[j+1]=k+1了吗?

3.由于tj!=tk,则等式"t0t1…tk"="tj-ktj-k+1…tj" 不成立,也就是说,在字符T[k+1]之前不存在一个子串" tj-ktj-k+1…tj"和子串"t0t1…tk"相等,那么是否可能存在另一个值p<k,使等式"t0t1…tp"=" tj-ptj-p+1…tj" 成立,这个p显然应该是 next[k+1],因为这相当于一个"利用next函数值进行T串和T串的匹配"问题。

编程实现求解next数组的代码:

/*
 * KMP求解next数组的方法
 */
int* getNext(char * t)
{
    //模式字符串长度
    int len =strlen(t);
    int *next=new int[len];

    /*将数组元素预置为0*/
    int i=0;
    while(t[i]!='\0')
    {
       next[i]=0;
       i++;
    }
    /*将数组元素预置为0*/

    int j=0;int k=-1;
    next[0]=-1;
    
    while(t[j+1]!='\0')
    {
        if(k==-1||t[j]==t[k])
        {
            k++;j++;
            next[j]=k;
        }        
        else
        {
            j++;
            k= next[k+1];
        }
    }    
    return next;
}

因此在朴素模式匹配算法的基础上我们通过新的移位方法可以改进得到KMP算法的实现:

/*
 * KMP模式匹配算法
*/
int kmpMatch(char* s,char* t)
{
    int j=0;
    int i=0;
    int *next=getNext(t);
    while(t[i]!='\0'&&s[j]!='\0')
    {
        if(s[j]==t[i])
        {
            i++;j++;
        }
        else {
            j=j-next[i];
            i=0;
        }
    }

    if(t[i]=='\0')
    { return j-i;}
    else
    { return -1;}

}

终于弄清楚KMP算法的精髓啦!

 

 

posted on 2016-10-26 14:47  shaozhuyong  阅读(1287)  评论(0编辑  收藏  举报