首页 > 技术文章 > 浅谈kmp

greenpepper 2019-04-18 10:35 原文

一、废话不多说,首先直白的说kmp是什么?

  先给你两个字符串,一个长的串一个短的串。 例:  abcdabccabca 与  cca,然后需要你求出第二个字符串第一个字符串中的位置

暴力匹配的话大概就是下文的算法

 1 for( int i =0 ; i < str1len ; i++){
 2        int flag=1;
 3     for( int j = 0 ; j < str2len ; j++){
 4         if( str1[ i+j ] != str2[ j ]{
 5             flag=0;
 6             break;
 7         }
 8     }
 9     if(flag)printf("%d ",i);
10 }
11 //strl1len 指length of string 1 也就是字符串1的长度。
12 //str1 指第一个字符串
13 //通常,在字符串匹配中,作者,默认:长串:str1,短串:str2
View Code

这种算法是大多数人都能想到的,时间复杂度约为O(mn)mn分别是两个字符串长度。

  而kmp算法就是针对这种串匹配问题的一种优化算法。

叫kmp的原因是因为发明这个算法的人三个巨巨名字首字母是分别kmp

继续读下去之前,请先确保对暴力匹配的算法进行理解。

二、为什么用kmp?

  kmp算法的核心在于对已匹配的信息充分利用。好吧,这么说太抽象了,举个具体的栗子吧。

  假设我们的第二个字符串是这样的。

  

  在与第一个字符串比较的时候,我们分别有两个字母, i代表在字符串1中匹配当前起始位置,j代表字符串2中匹配的位置。也就是上面暴力匹配代码中的str1 [ i+j ] == str2[ j ];

  现在假设我们字符串2中匹配到了这个位置 

 

  然后检测到不同,那么我们是不是应该将j回到头呢?不是的。

  红色划线部分是相同的对吧。这也就是意味着,如果我们j匹配到箭头所指的abc

  那么在字符串1中的i+j-1 i+j-2 i+j-3这三个元素一定也是abc。为什么? 因为我匹配过了啊。这就是我们所得到的已知信息。

  我们还已知什么呢?

  字符串2中的头三个字母 和目前失配的前三个字母是相同的。于是乎,我们可以把j移到前三个abc的后面也就是下图

  为什么?  因为你匹配到的当前后缀与str2的前缀有公共部分。所以匹配时,这些公共部分就可以当做匹配过。

  (后缀:就是从后往前的字符串,比如这个就是 a,ca,bca,直白的说就是后几个)

  (前缀:从前往后的字符串,比如a,ab,abc,直白的说就是前几个)

  这么做的好处是i每次到可以每次更新到i+j,不用回溯了。每次i只管往前走就行,j自己玩去。我们就省了很多的回溯时间。

  那么你一定又有一个疑问了。该怎么做呢?我怎么知道回到哪去呢???

三、怎么整这个算法啊??

  向下阅读请先对上方原理进行理解。

  那么我们可以着手上方的问题了,怎么知道可以回到哪去呢?

从下文起,i指目前匹配到的所在str1的位置, j 指目前匹配到 所在str2 的位置

  我们可以开出一个数组,在匹配前先对str2的每一位进行计算,也就是如果是第一位失配我该回到哪?第二位失配我该回到哪?and so on...

  读者在此像大部分kmp教程一样,将这个(对应了str2中,解释每一位该回到前面哪里去,也可以说是前缀和后缀的最长相同区域的)数组称作 next【】数组。

  比如还是上面那个字符串

  

  我们要求出他的每一位

  如果我们第一位失配,好吧我们别无选择,i进行下一位匹配吧,j不变。

  如果第二位失配,我们可以得到信息,b ,和前缀a不同。我们j回到最初的起点,(i到下一位)。

  如果第三位失配,我们已知信息中,第二位头不是a,所以无法匹配,所以i可以继续下一位。

  也就是说

  next[1] next[2] next[3]都是0, 直接回到了0处。

  以此类推,我们可以得到next数组为{0,0,0,0,0,1,2,3,}

  为了检测 j 在0点失配,我们规定 next[0]=-1;

  同时我们也可以发现,在求next数组时如果前一位最长前后缀是a,后一位也与前一位最长前缀的下一位匹配了,那么它的最长前后缀显然是a+1;

  比如 abcdab时,最长前后缀是2 当到下一位 abcdabc时,多出的c这一位与前一位前缀最后一位(绕的有点晕).

  就是说 abcdab 中的 前缀abc只有ab与后缀 dab的ab匹配了,那么  前缀ab的后一位c 与后缀的多出的一位c匹配, 这个多出的位最长公共前后缀就是比前一位多一位。

好了,这就是next数组的求法,下面给出代码。

void get_next()
{
    int i = 0, j = -1;
    nexxt[0] = -1;                         //对第一位初始化为-1;
    while (str2[i] != '\0') 
       {
        while (j != -1 && str2[i] != str2[j])//如果前后缀不相同了,
            j = nexxt[j];                      //看上一个最长前后缀
        
                nexxt[++i] = ++j;            //更新当前前后缀。
    }
}

有了这个数组,使用原理在第二部分也说过了,那么直接上使用的代码吧。

int kmp_1()
{
    int i=0,j=0;                              //各自从各自数组起点出发
    while(str1[i]!='\0')                   //检测字符串1是否到尾部
    {
        if(j==-1||str1[i] == str2[j] ){ //如果匹配,或者j在头开始的
            if(str2[j+1]=='\0')          //检测字符串2是否到尾,到了就说明
            {                                   //已经完全匹配了
                return i-j+1;              //返回字符串2在1 的位置
            }
            else j++;                      //没到头就继续匹配下一位
            i++;
        }
        else j=nexxt[j];                //j自己向前走,直到有公共前后缀,或
    }                                         //回到0处。

    return -1;                       //str1都匹配完了还没找到,只好返回-1咯
}

!!!注意:以上检测代码尾部的时候,若对时间要求高,则需要将检测尾部的

str1[i]!='\0'  和   str2[j+1]=='\0'  
提前将两个字符串长度存下来,也就是 i<str1len j+1<str2len,
否则会像我一样超时到自闭

 


 四、完整代码

  以下代码可以直接当做模板

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 using namespace std;
 5 int nexxt[100000005];
 6 char str2[100000005];
 7 char str1[100000005];
 8 int str1len, str2len;
 9 void get_next()
10 {
11     int i = 0, j = -1;
12     nexxt[0] = -1;
13     while (i < str2len) {
14         while (j != -1 && str2[i] != str2[j])
15             j = nexxt[j];
16         nexxt[++i] = ++j;
17     }
18 }
19 int kmp_1()
20 {
21     int i = 0, j = 0;
22     while (i < str1len)
23     {
24         if (j == -1 || str1[i] == str2[j]) {
25             j++;
26             i++;
27             if (j == str2len)return i - j + 1;
28         }
29         else j = nexxt[j];
30     }
31     return -1;
32 }
33 int main()
34 {
35     int n, a, b;
36     cin >> n;
37     while (n--)
38     {
39         getchar();
40         cin.getline(str1, 100000000, '\n');
41         cin.getline(str2, 100000000, '\n');
42         str1len = strlen(str1);
43         str2len = strlen(str2);
44         get_next();
45         cout << kmp_1() << endl;
46     }
47     return 0;
48 }

 

 

  

 

 

 如有疑问欢迎私信及评论。

  

 

推荐阅读