编程语言
首页 > 编程语言> > 字符串KMP算法

字符串KMP算法

作者:互联网

一、算法介绍:

KMP算法主要用于字符串包含问题,如:

        给定字符串A,B,判断B是否是A的子串

在这里,我们把等待匹配的字符串A称为母串,用来匹配的串B称为模式串

二、算法流程:

(感性理解???)


如果按照一般思路,我们循环A串的各个元素,判断是否包含B串,算法时间复杂度过高,因为这个算法忽略了之前我们已经对A,B串进行了比较的事实,所以在运行时进行了许多重复的判断,导致了时间复杂度过高。

因此,我们可以考虑对其进行优化~~

 

不要喷,我的图是搜的,我真心不会画图qwq

 

首先,来看一个例子。我们设A串(上面的串)的数组下标为A【1~n】,B串(下面的串)的数组下标B【1~m】

 

经过比较我们可以发现,当 i=4, j=4 时,字符串中的元素不一样了,也就是说我们现在找到的子串不合法了,所以为了节省时间,我们要跳过尽可能多的元素(因为经过比较我们发现A串中1~3个元素都与B串中第一个元素不一样了,所以不需要再浪费时间进行比较了),但是在跳过元素时也应当满足当有重复元素时,保留重复元素的特性,所以我们应该让B[1]与A[5]冲齐。

我们再看另一个例子

 

原来的字符串是这样的:

A  B  C  A  B  C  D  H  I  J  K

A  B  C  E

我们发现当元素A E不一样了,但是B串中的第一个元素A又可以和A串中的第四个元素A重合,所以我们就把字符串B移动到当前位置

再来一个例子!

 

我们发现此时元素C D 不一样,但是A串中的第三个元素A与B串中的第一个元素A是匹配的啊,所以,我们开始移动B串:

 

下一个例子也是一样滴:

 

 

这个例子不进行过多解释,还请读者自行思考(但是我相信 聪明的你们一定都会!!)  

(好吧这个例子好像挺重要的所以我还是讲一讲把TAT)

 

匹配之后我们发现元素C B不一样,但是我们不可以直接把B中的第一个元素A挪到第i+1个位置,因为那样做的话我们会失去许多重复元素(在这个题里是A[4~5]和B[1~2],都是A B)。

所以正确的做法是当存在重复元素时,让模式串与母串重合

 

从上面几个例子中我们可以找到一些规律:

当匹配失败后,j要移动的下一个位置K(在母串中的对应位置K哈,不是模式串),有着这样的关系:


最前面的K个字符和J之前的K个字符是一样的!!!

 

用数学公式表达就是:

 

 

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

 

这样的话,如果 我们首先设置 j 要移动下一个位置为p[j],那么我们就可以在母串中寻找模式串,代码如下:

j=0;
for(int i=1;i<=n;i++){
    while(j>0&&a[i]!=b[j+1]) j=p[j];//如果不相等,那么我们就回到重合部分的
                                //最后一个元素,从最后一个元素开始寻找 
    if(a[i]==b[j+1]) j++;
    if(j==0){
        cout<<i-m+1<<endl;//表示输出当前相同字符串的首字母下标 
        j=p[j];//因为可能存在多个包含关系,所以我们继续搜索 
    }
}

这样,我们就完成了在母串中对于模式串的查找。

But but,我们是不是忘记了什么重要的东西???

我们还没有说明怎么找到数组P啊!!!

重头戏来了(手动滑稽)

 

我们已经知道,P数组是用来寻找A B字符串重合元素的最后一个位置的数组变量(或许也可以称之为指针??) 那么其实....我们可以直接把P数组定义为:求模式串中的子串的真重复前缀和后缀(强烈建议停下来好好理解一下这句话)

我们来看一个例子:

 

 

我们通过A B串的对比可以发现,其实在不匹配元素之前,我们找到的A B子串是等价的,所以我们完全可以把找A B 串的重合部分(其实不太准确)转化为求B 串的前缀后缀(可爱)

我相信聪明的您们都已经明白了

所以,我们就相当于在一个字符串中再次运用KMP算法(其实整个KMP算法就是一个嵌套问题)

我自己造一个例子吧    唉~~

   1  2  3  4  5  6  7  8  9

B a  b  c  a  b  d  e  a  c

P 0  0  0  1  2  0  0  0  ?

就这个吧

 

P数组代表的是我们可以返回的元素的下标,(也可以理解为如果下一个元素不匹配,我们可以从当前元素找到一条“退路”),所以我们从头开始推P数组

因为B[1]=a,因为它是最左边的元素,所以不可能再有退路,所以它的返回值就是0(相当于返回到数组B[0])

因为B[2]=b,与B[1]=a不相等,所以如果我们发现元素不相等,我们应当再次返回B[0];

B[3]=c同理?

终于到了B[4]=a!!!(精神一震!!) 我们突然发现B[4]=a与B[1]=a相等,所以我们可以从B[4]跳到B[1];

B[5]=B[4+1]正好与B[2]=B[2+1]相等,也就是当前子串的前缀和后缀可以匹配,所以我们直接从B[5]返回B[2];(还记得我们的P数组返回的是重合元素的末尾地址吗?)

其它同理。各位大大可以自己多推几遍(掩盖住我其实并不想多写的事实)

 

好了!大体思路就是这样,代码实现如下

int j=0;
for(int i=2;i<=m;i++)//因为我们的i表示当前元素是否与j+1匹配
//所以我们从i=2开始循环 { while(j>0&&b[i]!=b[j+1]) j=p[j]; if(b[j+1]===b[i]) j++; p[i]=j; }//预处理模式串中所有的情况

 

总的来说,KMP和上面的代码其实还是很好理解的??(哼,才不告诉你我学了6h+呢)

其实各位大大们只要多看几遍题解+自己推几遍就可以理解了~~~~吧?

完整代码如下:

题目:洛谷P3375 【模板】KMP字符串匹配  https://www.luogu.com.cn/problem/P3375

#include<iostream>
#include<cstdio>
#include<bits/stdc++.h>
using namespace std;

int p[1020000],n,m,j;
char a[1020000],b[1020000];

int main() {
    cin>>a+1;
    cin>>b+1;
    n=strlen(a+1);
    m=strlen(b+1);
//    int j=0;
    for(int i=2; i<=m; i++) {
        while(j&&b[i]!=b[j+1]) j=p[j];
        if(b[j+1]==b[i]) j++;
        p[i]=j;
    }//预处理

    j=0;

    for(int i=1; i<=n; i++) {
        while(j>0&&a[i]!=b[j+1]) j=p[j];
        if(a[i]==b[j+1]) j++;
        if(j==m) {
//            cout<<" *";
            cout<<i-m+1<<endl;
            j=p[j];//再次跳回原来的地方,继续判断
        }
    }

    for(int i=1; i<=m; i++)
        cout<<p[i]<<" ";
    return 0;
}

——END——

 

标签:int,元素,算法,数组,KMP,字符串,串中,我们
来源: https://www.cnblogs.com/yxr001002/p/13368009.html