算法模板——KMP(字符串匹配)

 

看了算法书和博客,发现了next[]  可以有两种用法,不过kmp的算法思想都是一样的

1、入门介绍

什么是KMP算法:

KMP算法要解决的问题就是在字符串(也叫主串)中的模式(pattern)定位问题。说简单点就是我们平时常说的关键字搜索。

模式串就是关键字(接下来称它为P),如果它在一个主串(接下来称为T)中出现,就返回它的具体位置,否则返回-1(常用手段)。

 《算法模板——KMP(字符串匹配)》

首先,对于这个问题有一个很单纯的想法:从左到右一个个匹配,如果这个过程中有某个字符不匹配,就跳回去,将模式串向右移动一位。这有什么难的?

我们可以这样初始化:

 《算法模板——KMP(字符串匹配)》

之后我们只需要比较i指针指向的字符和j指针指向的字符是否一致。如果一致就都向后移动,如果不一致,如下图:

《算法模板——KMP(字符串匹配)》

 

A和E不相等,那就把i指针移回第1位(假设下标从0开始),j移动到模式串的第0位,然后又重新开始这个步骤:

 《算法模板——KMP(字符串匹配)》

对于常规的字符串匹配,一般都是一路穷举过去

可以看出常规的办法,每次都要从头开始检查,一旦检查到不相等,S串只能向右移一位,然后重新从头检查,做了很多重复的工作。

二、算法分析:

然而 KMP 算法的过程是这样的:

KMP核心在于next数组,主要有两种思想:

1、next数组存的是j下一步要移动到哪一个位置

(1)、

如果是人为来寻找的话,肯定不会再把i移动回第1位,因为主串匹配失败的位置前面除了第一个A之外再也没有A,我们为什么能知道主串前面只有一个A?因为我们已经知道前面三个字符都是匹配的!(这很重要)。移动过去肯定也是不匹配的!有一个想法,i可以不动,我们只需要移动j 即可

 

上面的这种情况还是比较理想的情况,我们最多也就多比较了再次。

但假如是在主串“SSSSSSSSSSSSSA”中查找“SSSSB”,比较到最后一个才知道不匹配,然后i回溯,这个的效率是显然是最低的。

大牛们是无法忍受“暴力破解”这种低效的手段的,于是他们三个研究出了KMP算法。其思想就如同我们上边所看到的一样:

 

“利用已经部分匹配这个有效信息,保持i指针不回溯,通过修改 j 指针,让模式串尽量地移动到有效的位置。”

 

所以,整个KMP的重点就在于当某一个字符与主串不匹配时,我们应该知道 j 指针要移动到哪

接下来我们自己来发现j的移动规律:

如图:C和D不匹配了,我们要把 j 移动到哪?显然是第1位。为什么?因为前面有一个A相同啊:

 《算法模板——KMP(字符串匹配)》

如下图也是一样的情况:

 《算法模板——KMP(字符串匹配)》

可以把 j 指针移动到第2位,因为前面有两个字母是一样的:

 《算法模板——KMP(字符串匹配)》

至此我们可以大概看出一点端倪,当匹配失败时,j要移动的下一个位置 k。

存在着这样的性质:最前面的k个字符和 j 之前的最后k个字符是一样的

如果用数学公式来表示是这样的

P[0 ~ k-1] == P[j-k ~ j-1]

这个相当重要,如果觉得不好记的话,可以通过下图来理解:

 《算法模板——KMP(字符串匹配)》

弄明白了这个就应该可能明白为什么可以直接将 j 移动到 k 位置了。

因为:

当T[i] != P[j]时

有T[i-j ~ i-1] == P[0 ~ j-1]

由P[0 ~ k-1] == P[j-k ~ j-1]

必然:T[i-k ~ i-1] == P[0 ~ k-1]、

公式很无聊,能看明白就行了,不需要记住。

这一段只是为了证明我们为什么可以直接将 j 移动到k而无须再比较前面的k个字符。

 

(2)、好,接下来就是重点了,怎么求这个(这些)k呢?

因为在P的每一个位置都可能发生不匹配,也就是说我们要计算每一个位置 j 对应的k,所以用一个数组next来保存,

介绍next:

(1)、j退到某next值时字符比较相等,指示器变量值各加1后继续比较;

(2)、next[j] = k,表示当 T[i] != P[j] 时,j 指针的下一个位置,也就是将要移动的位置

(3)、j退到-1的时候(即模式的第一个字符匹配失误时),i和j都要分别加1,表明从主串的下一个字符起和模式串进行匹配

(-1 只是定义的初值)

好,先把这个放一边,我们自己来推导思路,现在要始终记住一点,

next[j]的值(也就是k)表示,当P[j] != T[i]时,j指针的下一步移动位置

先来看第一个:当j为0时,如果这时候不匹配,怎么办?

 《算法模板——KMP(字符串匹配)》

像上图这种情况,j已经在最左边了,不可能再移动了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。

如果是当j为1的时候呢?

 《算法模板——KMP(字符串匹配)》

显然,j指针一定是后移到0位置的。因为它前面也就只有这一个位置了~~~

 

下面这个是最重要的,请看如下图:

 《算法模板——KMP(字符串匹配)》 《算法模板——KMP(字符串匹配)》

 

请仔细对比这两个图。

我们发现一个规律:

当P[k] == P[j]时,

有next[j+1] == next[j] + 1

其实这个是可以证明的:

因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)

这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。

即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1。

这里的公式不是很好懂,还是看图会容易理解些。

 

那如果P[k] != P[j]呢?比如下图所示:

《算法模板——KMP(字符串匹配)》

像这种情况,如果你从代码上看应该是这一句:k = next[k];为什么是这样子?你看下面应该就明白了。

 《算法模板——KMP(字符串匹配)》

现在你应该知道为什么要k = next[k]了吧!像上边的例子,我们已经不可能找到[ A,B,A,B ]这个最长的后缀串了,但我们还是可能找到[ A,B ]、[ B ]这样的前缀串的。所以这个过程像不像在定位[ A,B,A,C ]这个串,当C和主串不一样了(也就是k位置不一样了),那当然是把指针移动到next[k]啦。

有了next数组之后就一切好办了,我们可以动手写KMP算法了:

 

(3)、CODE:

 

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
char s[1000001],c[1000001];
int next[1000001];
void nextt()
{
    int i=0;    // 指向主串的位置
    int j=-1;   // 指向模式串的位置
    int m;

    next[0]=-1;  // 初值

    m=strlen(s);
    while(i<m){
        if(j==-1||s[i]==s[j]){  // 当j等于-1的时候,移动i,当然j也要加1
            i++;
            j++;
            next[i]=j;
        }
        else
            j=next[j]; // 不相等的时候,移动j的位置,i不动
    }
}

int KMP()
{
    int i=0,j=0,sum=0;
    int n,m;

    m=strlen(s);
    n=strlen(c);
   

    nextt();

    while(i<n&&j<m)
    {
        if(j==-1||c[i]==s[j]) // 相等的时候加1
            i++,j++; 
        else
            j=next[j];     //  不相等的时候移动j的位置

        if(j==m){        //   输出主串中与模式串相等的个数
            sum++;
            j=next[j];
       pos=i-j;          //   pos 为相等的那个子串的第一个位置
        }
    }
    return sum;
}
int main()
{
    int t,i,sum=0;
    scanf("%d",&t);
    while(t--){
        sum=0;
        scanf("%s %s",s,c);
        sum=KMP();
        printf("%d\n",sum);
    }
}

 

2、next数组存的是每一位的最大相同前后缀的长度

(1)、介绍

1

首先,字符串”BBC ABCDAB ABCDABCDABDE”的第一个字符与搜索词”ABCDABD”的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。

2

因为B与A不匹配,搜索词再往后移。

3.

就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。

4.

接着比较字符串和搜索词的下一个字符,还是相同。

5.

直到字符串有一个字符,与搜索词对应的字符不相同为止。

6.

这时,最自然的反应是,将搜索词整个后移一位,再从头逐个比较。这样做虽然可行,但是效率很差,因为你要把”搜索位置”移到已经比较过的位置,重比一遍。

7.

一个基本事实是,当空格与D不匹配时,你其实知道前面六个字符是”ABCDAB”。KMP算法的想法是,设法利用这个已知信息,不要把”搜索位置”移回已经比较过的位置,继续把它向后移,这样就提高了效率。

8.

怎么做到这一点呢?可以针对搜索词,算出一张《部分匹配表》(Partial Match Table)。这张表是如何产生的,后面再介绍,这里只要会用就可以了。

9.

已知空格与D不匹配时,前面六个字符”ABCDAB”是匹配的。查表可知,最后一个匹配字符B对应的”部分匹配值”为2,因此按照下面的公式算出向后移动的位数:

  移动位数 = 已匹配的字符数 – 对应的部分匹配值

因为 6 – 2 等于4,所以将搜索词向后移动4位。

10.

因为空格与C不匹配,搜索词还要继续往后移。这时,已匹配的字符数为2(”AB”),对应的”部分匹配值”为0。所以,移动位数 = 2 – 0,结果为 2,于是将搜索词向后移2位。

11.

因为空格与A不匹配,继续后移一位。

12.

逐位比较,直到发现C与D不匹配。于是,移动位数 = 6 – 2,继续将搜索词向后移动4位。

13.

逐位比较,直到搜索词的最后一位,发现完全匹配,于是搜索完成。如果还要继续搜索(即找出全部匹配),移动位数 = 7 – 0,再将搜索词向后移动7位,这里就不再重复了。

14.

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

首先,要了解两个概念:“前缀”和”后缀”。

“前缀”指除了最后一个字符以外,一个字符串的全部头部组合;”后缀”指除了第一个字符以外,一个字符串的全部尾部组合。

15.

“部分匹配值”就是”前缀”和”后缀”的最长的共有元素的长度。以”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。

16.

“部分匹配”的实质是,有时候,字符串头部和尾部会有重复。

比如,”ABCDAB”之中有两个”AB”,那么它的”部分匹配值”就是2(”AB”的长度)。

搜索词移动的时候,第一个”AB”向后移动4位(字符串长度-部分匹配值),就可以来到第二个”AB”的位置。

(2)、next分析:

 

void makeNext(const char P[],int next[])
{
    int q,k;             //q:模版字符串下标;k:最大前后缀长度
    int m = strlen(P);  //模版字符串长度
    next[0] = 0;        //模版字符串的第一个字符的最大前后缀长度为0
    for (q = 1,k = 0; q < m; ++q)    //for循环,从第二个字符开始,依次计算每一个字符对应的next值
    {
        while(k > 0 && P[q] != P[k]) //递归的求出P[0]···P[q]的最大的相同的前后缀长度k
            k = next[k-1];         
      
        if (P[q] == P[k])            //如果相等,那么最大相同前后缀长度加1
        {
            k++;
        }
        next[q] = k;
    }
}

 

现在我着重讲解一下while循环所做的工作:

  1.   已知前一步计算时最大相同的前后缀长度为k(k>0),即P[0]···P[k-1];
  2.   此时比较第k项P[k]与P[q],如图1所示
  3.   如果P[K]等于P[q],那么很简单跳出while循环;
  4.   关键!关键有木有!关键如果不等呢???那么我们应该利用已经得到的next[0]···next[k-1]来求P[0]···P[k-1]这个子串          中最大相同前后缀,可能有同学要问了——为什么要求P[0]···P[k-1]的最大相同前后缀呢???是啊!为什么呢? 原因          在于P[k]已经和P[q]失配了,而且P[q-k] ··· P[q-1]又与P[0] ···P[k-1]相同,看来P[0]···P[k-1]这么长的子串是用不了了,那么我要找个同样也是P[0]打头、P[k-1]结尾的子串即P[0]···P[j-1](j==next[k-1]),看看它的下一项P[j]是否能和P[q]匹配。如图2所示

完整CODE:

#include<stdio.h>
#include<string.h>
void makeNext(const char P[],int next[])
{
    int q,k;
    int m = strlen(P);
    next[0] = 0;
    for (q = 1,k = 0; q < m; ++q)
    {
        while(k > 0 && P[q] != P[k])
            k = next[k-1];
        if (P[q] == P[k])
        {
            k++;
        }
        next[q] = k;
    }
}

int kmp(const char T[],const char P[],int next[])
{
    int n,m;
    int i,q;
    n = strlen(T);
    m = strlen(P);
    makeNext(P,next);
    for (i = 0,q = 0; i < n; ++i)
    {
        while(q > 0 && P[q] != T[i])
            q = next[q-1];
        if (P[q] == T[i])
        {
            q++;
        }
        if (q == m)
        {
            printf("Pattern occurs with shift:%d\n",(i-m+1));
        }
    }    
}

int main()
{
    int i;
    int next[20]={0};
    char T[] = "ababxbababcadfdsss";
    char P[] = "abcdabd";
    printf("%s\n",T);
    printf("%s\n",P );
    // makeNext(P,next);
    kmp(T,P,next);
    for (i = 0; i < strlen(P); ++i)
    {
        printf("%d ",next[i]);
    }
    printf("\n");

    return 0;
}

 

 《算法模板——KMP(字符串匹配)》《算法模板——KMP(字符串匹配)》

    原文作者:KMP算法
    原文地址: https://blog.csdn.net/JKdd123456/article/details/82994486
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞