改进的模式匹配(KMP)算法

一、简介

在说改进的模式匹配(KMP)算法之前我们先说朴素的模式匹配:
其实很简单,就是两个字符串逐位比较。在模式匹配中:我们假定字符串P在字符串T中查找是否有匹配的。此时,称P为模式(Pattern)字符串,称T为目标(Target)字符串。
OK,我一般比较喜欢以实例说明问题。
T: a b d a b d a b c
P: a b d a b c
朴素的模式匹配算法
朴素的模式匹配算法就是用P和T依次比较,即为:
第一趟比较:

 T:        a  b  d  a  b  d  a  b  c 
 P:        a  b  d  a  b  c
           发现第6个元素(下标为5)d和c不相等,第一趟结束,此时比较了6次(6个元素)。

第二趟比较:

   T:        a  b  d  a  b  d  a  b  c 
   P:           a  b  d  a  b  c
               第一个元素就不相等,第二趟结束,此时比较了1次(1个元素)。

第三趟比较:

T:        a  b  d  a  b  d  a  b  c 
   P:              a  b  d  a  b  c
     第一个元素就不相等,第三趟结束,此时比较了1次(1个元素)。

第四趟比较:

T:        a  b  d  a  b  d  a  b  c 
   P:                 a  b  d  a  b  c
     第一个元素相等,第二个元素也相等,第三、四、五、六都相等,匹配成功,第四趟结束,此时比较了6次(6个元素)。

匹配成功,共比较14次。但是这个是我们理想状态下的匹配方案,实际中字符串的长度远远不止这些。这种算法是一种带回逆的算法,成为朴素的模式匹配算法。

改进的模式匹配(KMP)算法
KMP算法就是消除了朴素匹配的回逆,利用一个失效函数(failure function)替代直接的回逆。思路如下:
第一趟比较:
T: a b d a b d a b c
P: a b d a b c
发现第6个元素(下标为5)d和c不相等。此时,进入一个P串的处理:
此时取出P串, a b d a b c 因为是c不和d不匹配,去掉此项,获得
a b d a b
此时判断 a b d a 是否与 b d a b 相等? 不等,进入下一轮判断
此时判断 a b d 是否与 d a b 相等? 不等,进入下一轮判断
此时判断 a b 是否与 a b 相等? 相等,结束第一趟总体判断。
(先不要急,接下来我就会说为什么这样匹配和这样匹配的用途!)
然后直接拿d和字符串中不匹配的那一项进行比较。
以上就是KMP的流程,为什么要这样做?在一些串中,目标串会远远长于模式串,如果每次都江模式串和目标串一一比较。此时时间复杂度当增加,而且在模式串中会出现很多的无效匹配,相当于无用功。但是假如先在模式串中进行比较,因为模式串会远远短于目标串,所以会相当减少时间复杂度。

二、next数组的理解

对于KMP算法来说,重点就是 next数组 (也有叫覆盖函数,部分匹配表,lps数组等)。

总之就是 对模式串做预处理,而且该预处理只和 模式串(pattern)本身有关!

假设有模式串 pattern = “abababca”; 则有匹配表:

char:  | a | b | a | b | a | b | c | a |
index: | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value: | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 |

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

首先,要了解两个概念:”前缀”和”后缀”。 “前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。

字符串:   "bread"

前缀: b , br, bre, brea

后缀:  read, ead, ad ,  d

关于 next数组 (也有叫覆盖函数,部分匹配表,lps数组) 的通俗解释:”部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”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。

下面举个例子

pattern “AABAACAABAA”, next[] is [0, 1, 0, 1, 2, 0, 1, 2, 3, 4, 5]
pattern “ABCDE”, next[] is [0, 0, 0, 0, 0]
pattern “AAAAA”, next[] is [0, 1, 2, 3, 4]
pattern “AAABAAA”, next[] is [0, 1, 2, 0, 1, 2, 3]
pattern “AAACAAAAAC”, next[] is [0, 1, 2, 0, 1, 2, 3, 3, 3, 4]

代码如下:

/*
* function          生成部分匹配表,next数组
* param     subStr  模式串,子串
* param     next    next数组,部分匹配信息表
* param     len     next数组的长度,一般就是模式串的长度
* return            无
*/
void GetNext(const char* subStr, int* next, int len)
{
    memset(next, 0, len);
    int prefix = -1; //前缀
    int suffix = 0; //后缀
    next[0] = -1;    //第一个元素只是用来控制prefix和suffix后移的
    while (suffix < len - 1)//当比较到最后一个字符的时候退出循环
    {
        /*
        当prefix == -1的时候表示要从prefix=0,suffix=1开始比较
        若prefix != -1,表示前缀和后缀已经有重合的了,接着往后移比较
        例如:subStr="ABABABB"
            1.prefix=-1,往后移,prefix=0,suffix=1,next[1] = 0,表示字符串‘A’前缀后缀无重合

            2.prefix=0,比较subStr[0]和subStr[1]('A'和'B'),不相等,把prefix重新置为next[prefix](next[0]==-1)

            3.prefix=-1,往后移,prefix=0,suffix=2,next[2] = 0,表示字符串‘AB’前缀后缀无重合

            4.prefix=0,比较subStr[0]和subStr[2]('A'和'A'),相等,继续往后移,prefix=1,suffix=3,next[3]=1
              表示字符串"ABA"有一个字符前缀后缀相等('A'和'A')

            5.prefix=1,比较subStr[1]和subStr[3]('B'和'B'),相等,继续往后移,prefix=2,suffix=4,next[4]=2
              表示字符串"ABAB"有两个字符前缀后缀相等('AB'和'AB')

            6.prefix=2,比较subStr[2]和subStr[4]('A'和'A'),相等,继续往后移,prefix=3,suffix=5,next[5]=3
              表示字符串"ABABA"有三个字符前缀后缀相等('ABA'和'ABA')

            7.prefix=3,比较subStr[3]和subStr[5]('B'和'B'),相等,继续往后移,prefix=4,suffix=6,next[6]=4
              表示字符串"ABABAB"有四个字符前缀后缀相等('ABAB'和'ABAB')

            8.当suffix=6最后一个的时候,就不需要比较了,因为KMP算法中最后一个并无指导匹配的作用,因为一旦前6个匹配成功,最后一个
              就算不成功,用到的也是前一个的部分匹配信息,若是成功那就直接返回了,所以求next数组的时候,最后一个的信息省略  

        */
        if (prefix == -1 || subStr[prefix] == subStr[suffix])
        {
            ++prefix, ++suffix;           
            next[suffix] = prefix;
            printf("%d ", next[suffix]);  //测试用,可删除
        }
        else
            prefix = next[prefix];
    }
    printf("\n");     //测试用,可删除
}

三、KMP模式匹配算法

int Index_KMP(char *s,char *p,int pos,int next[])
/*利用模式串p的next函数,求p在主串中从第pos个字符开始的位置*/
/*若匹配成功,则返回模式串在主串中的位置(下标),否则返回-1。设模式串第一个字符的下标为0*/
{
  int i,j,slen,plen;
  i = pos-1;
  j=-1;
  slen = strlen(s);plen = strlen(p);
  while(i=plen)return i-plen;
  else return -1;

你可能感兴趣的:(改进的模式匹配(KMP)算法)