KMP算法(串的模式匹配)

在学习KMP算法之前还是希望能够理解BF算法,也就是暴力的算法。

如果感到不适应还希望坚持一下,也许下一秒你就懂了。

前置概念:

在说kmp算法之前我们先介绍介个概念.

1.文本串:文本串就是我们所说的主串。

2.模式串:模式串就是我们所说的子串。(那么kmp算法实现的就是要在文本串当中查找模式串的位置)。

3.前缀和后缀:

假设现在有一个字符串s,长度是n。

①前缀:那么我们说字符串s的所有前缀为{s1, s1s2, s1s2s3, s1s2s3s4s5, ......, s1s2s3...s(n-1)};

②后缀:同上我们说字符串s的所有后缀为{s(n), s(n-1)sn, s(n-2)s(n-1)s(n), ......, s2s3s4s5...s(n)};

不难发现实际上前缀的集合是去掉最后一个字符的所有头部的集合,后缀的集合是去掉头部之后的所有尾部的集合。

那么根据上面的分析单个字符是没有前缀与后缀的,也就是如果n等于1的时候,s的前缀与后缀的集合都是空集。

我们现在来举例说明:

假设现在有字符串ABABAAB。

所有前缀:A,AB,ABA,ABAB,ABABA,ABABAA。

所有后缀:B,AB,AAB,BAAB,ABAAB,BABAAB。

4.前缀后缀最长公共元素长度:

根据上面的例子,字符串ABABAAB的前缀后缀最长公共元素为AB,那么他的长度就是2,。

在这里一定要好好理解前缀后缀最长公共元素,这个实际上是整个KMP算法的核心,解析模式串,实际上就是解析模式串的浅灰与后缀。

算法思想:

首先我们要知道BF算法的思想,BF算法的思想就是对于文本串的每一个位置,都假设他是模式串的开始位置,然后依次匹配文本串与模式串的各个字符,如果在匹配的过程当中存在某个字符不相等,那么我们就进行回溯(对于回溯的概念实际上还是比较重要的,因为在dfs当中回溯是必须的),然后重复操作,知道模式串的指针指向模式串的尾部位置。那么如果我们考虑最坏的情况,也就是到了最后我才知道模式串不在文本串当中,这个时候的算法复杂度就是O(n*m)。

    i
A B A B C A B C A C B A B
A B C
    j

也就是如果我们的匹配出现了上面这样的情况,我们下一步应该是:

  i
A B A B C A B C A C B A B
  A B C
  j

从这里重新开始匹配,那么我们不难发现如果文本串的指针i老是回溯的话是非常浪费时间的,并且我们能够发现在第一次匹配完成结束时,我们就知道,文本串的第二个字符与模式传的第一个字符不相等。

KMP正是实现了文本串的指针i不回溯来达到降低时间复杂度的。

                  i
B B C A B C D A B A B C D A B C D A D E
      A B C D A B D
                  j

假设我们现在匹配到了这里,那么我们不在考虑BF算法,那么如果是KMP算法我们应该怎么办呢?(先别管他是怎么计算出模式串如何移动的,先清楚为什么这么移动)。

                  i
B B C A B C D A B A B C D A B C D A D E
              A B C D A B D
                  j

那么如果是KMP算法我们下一步的匹配应该是这样的,到了这里你发现了什么没有?实际上这里就是整个算法最核心的思想,也就是前缀后缀的最长公共元素到底有什么用的问题。

我们不妨分析一下模式串的前缀后缀最长公共元素长度。

我们假设用S表示模式串。

S[0]的前缀后缀最长公共元素的长度就是0.

S[0]s[1]的前缀后缀最长公共元素的长度也是0,S[0]s[1]s[2]的前缀后缀最长公共元素的长度也是0,s[0]s[1]s[2]s[3]的最长公共元素的长度是0,s[0]s[1]s[2]s[3]s[4]的最长公共元素长度是1,s[0]s[1]s[2]s[3]s[4]s[5]的最长公共元素长度是2,s[0]s[1]s[2]s[3]s[4]s[5]s[6]的最大公共元素长度是0.

A B C D A B D
0 0 0 0 1 2 0

那么我们再回过头来看刚才的情况

                  i
B B C A B C D A B A B C D A B C D A D E
      A B C D A B D
      0 0 0 0 1 2 0                  
                  j

首先我们要想明白一个地方也就是当模式串j到达某个位置的时候我们可以知道前j-1个字符是完全相同的。

我们再去想前缀后缀最长公共元素,既然是公共元素那么必然是相同的,因此我们发现,如果当前不匹配,我们可以把以前一个字符结尾的后缀对应的最长的前缀移到当前的位置,使得文本串的指针不动,也就是:

                  i
B B C A B C D A B A B C D A B C D A D E
              A B C D A B D
              0 0 0 0 1 2 0                  
                  j

那么我们发现这样移动可以使得i不再回溯,并且保证了前j-1个字符相同。

到这里你是否能够想明白前缀后缀最长公共元素的长度有什么用了呢?

next数组:

上面我们分析了KMP算法的核心思想,并且涉及到了前缀后缀最长公共元素的长度的作用这样的问题,那么在理解这些之后,我们需要考虑的一个问题就是,对于模式串的指针j如果当前元素不匹配我们如何移动的这样的问题。

那么我们定义next数组也就是如果当前的字符如果不匹配下一次对应的位置应该是j = next[j],那么下面问题来了next数组到底怎么求?

实际上根据上面的例子我们知道,如果j所对应的元素不匹配,我们应该是让j等于前一个元素的前缀后缀最长公共元素的长度,想想是不是这样?

因此next数组我们只需要将前缀后缀最长公共元素的长度后移即可。

                  i
B B C A B C D A B A B C D A B C D A D E
              A B C D A B D
              0 0 0 0 1 2 0   
        next -1 0 0 0 0 1 2 0              
                  j

这样当不匹配的时候我们就可以让j = next[j]了,计算出下一个应该去比较的位置。

为什么第一个next是-1呢?我们待会再来考虑这个问题。

next数组的计算:

void getNext()
{
    int i = 0, j = -1;
    Next[i] = j;
    while(i < pattern.length())
    {
        if(j == -1 || pattern[j] == pattern[i])
            Next[++i] = ++j;
        else
            j = Next[j];
    }
}

先把代码放出来,我们来分析(代码中的Next的下标与下文的next的下标要区别开来)。

我们假设对于模式串有p[0]p[1]p[2]...p[k-1] = p[j-k]p[j-k+1]...p[j-1];

也就是说p[k-1] = p[j - 1],因此p[0]p[2].....p[j-1]的前缀后缀最长公共元素长度就是k,那么如果在做匹配的时候p[j]不匹配,应该是使得j = k,也就是next[j] = k,也就不难解释为什么是Next[++i] = ++j了,类比上去就好了。

那么我们再来分析如果p[k-1] != p[j-1]的时候,那么这个时候next[j]显然不是k了,我们可以考虑这样的一个问题,当文本串与模式串不匹配应该怎么办?没错就是模式串在next数组当中回溯,让k-1 = next[k-1],看看这个时候是否匹配。也就是else的句子。

最后的一个问题-1是干什么的,实际上-1是用来解决第一个元素就不一样的这个问题的。

      i
A B C D A B D
      A B C D A B D
      j

也就是这样的情况,显然是i后移,j不动。

        i
A B C D A B D
        A B C D A B D
        j

对应到两个串都是模式串就好了。

KMP算法:

实际上求next就是KMP,KMP与求next是一样的。

bool kmp()
{
    int i = 0, j = 0;
    while(i < text.size() && j < pattern.size())
    {
        if(j == -1 || text[i] == pattern[j])
        {
            j++;
            i++;
        }
        else
            j = Next[j];
    }
    if(j >= pattern.size())
        return true;
    return false;
}

这里就不在赘述了。

总结:

实际上KMP的关键就是前缀后缀最长公共元素的长度来计算出j的下一个位置。

最后说一下如果数组在代码里直接写成next可能会ce哦。

你可能感兴趣的:(Data,Structure)