KMP匹配模式算法

简介
就是几个科学教觉着暴力匹配字符串太磨叽,在一块研究了个新算法,这三个前辈分别是D.E.Knuth,J.H.Morris以及V.R.Pratt,所以这个算法叫做克努特-莫里斯-普拉特算法,简称kmp算法
核心思想
主字符串挨个递增不回溯,子串引入一个next数组,用来记录当字符不匹配时子串应该回溯到的合适位置
算法过程
其实KMP算法也是从暴力匹配算法基础上改进的一个算法,所以我们先从暴力匹配过程中了解kmp算法的过程
举例:
主串 S = “abcdefgab”
模式串 T = “abcdex”
暴力匹配算法过程
i表示主串位置,j表示模式串位置

  1. 前五位匹配,第六位字符不匹配,此时i=5, j=5
    a b c d e f g a b
    a b c d e x

  2. 此时i=1, ,j=0;
    a b c d e f g a b
      a b c d e x    
    3.i=2, ,j=0;
    a b c d e f g a b
        a b c d e x  
    4.i=3, ,j=0;
    a b c d e f g a b
          a b c d e …
    5.i=4, ,j=0;
    a b c d e f g a b
            a b  c   d  …

  3. i=5, j=0;
    a b c d e f g a b
              a   b c  … 
    在上面的匹配过程中,子串中 “a”与后面的“bcdex”均不相等,即
    T[0] != T[1]
    T[0] != T[2]
    T[0] != T[3]
    T[0] != T[4]
    在第一步的时候已经判断过
    S[0]==T[0]
    S[1]==T[1]
    S[2]==T[2]
    S[3]==T[3]
    S[4]==T[4]
    所以就有
    T[0] != S[1]
    T[0] != S[2]
    T[0] != S[3]
    T[0] != S[4]
    再然后2,3,4,5步完全没必要啊,有了步骤1(注意这里是前提),我直接走步骤6就好了呀
    (为啥6要保留呢,因为T[0] !=T[5] 而在第一步T[5] != S[5] 所以没法知道T[0]和S[0]的关系)
    从步骤1到6,主串i值又回到了5,它不回溯,只考虑j值就行
    前面说道判断了T[0]和后面的字符都不相等,如果存在相等的可咋办
    看这个例子
    主串 S = “abcababca”
    模式串 T = “abcabx”

  4. 前五位匹配,第六位字符不匹配,此时i=5, j=5
    a b c a b a b c a
    a b c a b x

  5. i=1, ,j=0;
    a b c a b a b c a
      a b c a b x    
    3.i=2, ,j=0;
    a b c a b a b c a
        a b c a b x  
    4.i=3, ,j=0;
    a b c a b a b c a
          a b c a b x
    5.i=4, ,j=1;
    a b c a b a b c a
          a b c a b x
    6.i=5, ,j=2;
    a b c a b a b c a
          a b c a b x
    依照我们第一种比较的方法所以2,3,步骤是可以省掉的这里不再赘述
    到了4,5也是同样的道理,子串T[0]==T[3] 第一步T[3]==S[3] 所以T[0]==S[3]
    最后直接到第六步

总结 综合1,2两个例子我们发现一旦出现不匹配的情况,由不匹配的那一位来决定子串应该跳到合适的位置,
至于跳到什么位置,由子串的重复程度来决定,并且是不匹配字符之前的子串重复程度,重复度越高,跳的越远即距离首字符的位置,
(后面讲对这种高重复度的模式再进行一次优化)

KMP算法引入了next数组用来记录没一个字符如果跟主串不匹配了,我该跳到什么位置

位置的确定:子串前缀后缀的集合中最长公共串的长度
这里引入两个概念
前缀:不包含最后一个字符且必须包含第一个字符的顺序串
比如:ABDD的前缀A,AB,ABD
后缀:不包含第一个字符且必须以最后一个字符结尾的顺序串
比如:ABDD的后缀D,DD,BDD
根据总结
1,2两个例子对应的next值为
  a b c d e x
next[i] -1 0 0 0 0 0

其中
子串第一位a以前没有字符串,无意义用-1表示
第二位b以前的字符串为“a”,无前缀,无后缀,没有公共串,用0表示
第三位c以前的字符串为“ab”,前缀“a”,后缀“b”,没有相同子串,表示为0
依次类推
  a b c a b a b c a
next[i] -1 0 0 0 1 2 1 2 3
其中
子串第一位a以前没有字符串,无意义用-1表示
第二位b以前的字符串为“a”,无前缀,无后缀,没有公共串,用0表示
第三位c以前的字符串为“ab”,前缀“a”,后缀“b”,没有相同子串,表示为0
第四位a以前的字符串为“abc”,前缀“a”,“ab”,后缀"c",“bc”,没有相同串,为0
第五位b以前的字符串为“abca”,前缀"a",“ab”,“abc”,后缀"a",“ca”,“bca”,最长公共串为"a",长度为1,用1表示
第六位a以前的字符串为“abcab”,前缀"a",“ab”,“abc”,“abca”,后缀"b",“ab”,“cab”,“bcab”,最长公共串为"ab",长度为2,用2表示
以此类推
现在有这用一种情况
T串 a a a a c
next[i] -1 0 1 2 3
试想一下,如果这个字符串跟一个子串比较,比如在第四字符T[3]的时候不匹配
按照“a”对应的next[2]即跳到T[2]的位置然后在去与主串比较
你认为有必要吗?
肯定又是在浪费工夫,因为T[0] == T[1] == T[2] == T[3],如果T[3]不匹配了,按照这个前提就应该找与T[3]相同的值,所以如果相同那么就找到它的“父亲”,如果它“父亲”也相同的就去找它“爷爷”直到它的“祖宗”
既然是这样,我们就在刚开始遍历的时候就可以加上,如果字符的”孩子”(下一个字符)长得像它自己(T[n] == T[n+1]),就把自己的 j 给孩子(next[i] = next[j]),这样一层一层的给下去,不远多远的亲戚通过这个 j 就能直接找到祖宗了
代码如下
void getNext(char* s, int* next)
{
int len = strlen(s);
int i, j;
i = 1;
j = 0;
next[0] = -1;
next[1] = 0; //初始化
while (i < len)
{
if (j == -1 || s[i] == s[j])
{
++i;
++j;
if (s[i] == s[j])
{
next[i] = next[j];
}
else
next[i] = j;
}
else
{
j = next[j];
}
}
}

在查找匹配字符串的时候
// 返回T在S中的位置,如果没有返回-1
int indexKmp(char* S, char* T)
{
int i = 0;
int j = 0;
int slen = strlen(S);
int tlen = strlen(T);
int next[32];
int nextval[32];
getNext(T, next);
while (i < slen && j < tlen)
{
if (j == -1 || S[i] == T[j])
{
++i;
++j;
}
else
{
j = nextval[j];
}
}
if (j == tlen)
{
return i - strlen(T);
}
else
{
return -1;
}
}

猜你喜欢

转载自blog.csdn.net/weixin_41369611/article/details/94723311