• 浅谈kmp


    一、废话不多说,首先直白的说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 }

      

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

      

  • 相关阅读:
    一个用户下表、批量授予权限给另一个用户
    查询表使用率
    查询临时表空间大小及压缩空间大小
    创建表空间
    About SSDT BI
    在Win8中用批处理创建Oracle数据库时报“Unable to open file”
    收藏网址
    shell输入与输出功能
    shell变量类型和运算符
    shell文件权限和脚本执行
  • 原文地址:https://www.cnblogs.com/greenpepper/p/10728147.html
Copyright © 2020-2023  润新知