KMP算法详解

Posted teos

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KMP算法详解相关的知识,希望对你有一定的参考价值。

KMP算法, 又称模式匹配算法,能快速判断字符串b是否为字符串a的子串。设a的长度为N,b的长度为N,则KMP算法的时间复杂度为O(N+M)。


在讲解KMP算法之前,先将一种易懂的解决这类问题的方法:枚举a的每个元素$a_i$,每次枚举时比较$a_i$与$b_1,a_i+1$与$b_2$,...,$a_i+N-1$与$b_N$是否相等,若全部相等,则b为a的子串。时间复杂度O(NM);

显然这个方法太慢了,因此我们需要KMP算法来更高效地解决这类问题。当然,用Hash也可以解决这类问题,不过用KMP算法会更优一些。


若字符串b为a的子串,则显然a中存在至少存在一段字符与b的所有前缀相匹配;若这段字符的长度等于N,则b为a的子串。因此我们定义一个f数组,$f_i$表示a中以i结尾子串与b的前缀匹配的最长长度。

如何进行匹配呢?若当前以i结尾的长度为j的a的子串与b的长度为i的前缀匹配,则继续比较$a_i+1$与$b_a+1$是否相等,若相等则扩展子串长度,若不相等则需要缩小j,继续进行匹配。

如何缩小j呢?若一个一个地缩小j,显然效率太低。我们可以发现,当a[i-j~i]与b[1~j]匹配时,若有b[1~k]与b[i-k~i]匹配,且有b[k-l~k]与b[1~l]匹配,则有b[i-l~l]与b[1~l]匹配。因为若b[1~k]与b[i-k~i]匹配,说明k之前包含k的l个字符和i之前包含i的l个字符是相同的,也就是b[k-l~k]与b[i-l~i]匹配,所以有b[i-l~l]与b[1~l]匹配。为了使枚举的长度尽量地长,因此我们需要找到一个最大的符合条件的l。这个用和上一段的匹配非常类似的递推就可以实现了。

首先,我们定义一个数组next,$next_i$表示b中以i结尾的非前缀子串与b的前缀匹配的最长长度。若当前以i为结尾的长度为j的b的非前缀子串与b的长度为i的前缀匹配,则继续比较$b_i+1$与$b_j+1$是否相等,若相等则扩展子串长度,若不相等则缩小j,继续进行匹配。

在这里又如何缩小j呢?因为我们递推时是按照下标升序进行的,因此next[1~j-1]都已求出,所以我们直接取j=next[j]就可以了。如果不断地缩小j都无法匹配,则从头开始。

递推next数组代码:

void pre()

    next[0]=-1;//初始化
    for(int i=1,j=-1;i<b.size();i++)
    
        while(j>-1 && b[i]!=b[j+1])
            j=next[j];
        if(b[i]==b[j+1])
            j++;//若匹配,则继续比较下一个
        next[i]=j;//维护数组
    

递推出next数组后,进行匹配递推f数组就非常简单了。因为两个递推思想类似,因此代码也十分相似。

递推f数组代码:

void calm()

    for(int i=0,j=-1;i<a.size();i++)
    
        while(j>-1 &&(j==b.size()-1 || a[i]!=b[j+1]))//若j==b.size()-1则说明b在a中出现
            j=next[j];
        if(a[i]==b[j+1])
            j++;
        f[i]=j+1;//记录下位置
    

总体实现过程如下图所示:

技术图片

本文的代码均使用string类型存储字符串,而string类型的字符串下标是从0开始的,但题目中的位置大多从1开始,因此在很多地方需要进行特殊处理。这些处理会在下面的代码中一一说明。

next数组和f数组的定义大小:通过上面的讲解应该很明显了,定义next[M],f[N]。在代码实现中,为了防止出锅,应该把数组定义得略大一些。

完整代码:

#include<iostream>
#include<string>
using namespace std;
const int N=2e6;
int next[N],f[N];
string a,b;
void pre()

    next[0]=-1;//按照定义应该为0,但是因为next数组在实现过程中起指针作用,应该在原基础上减1;或在实现过程中加1亦可。
    for(int i=1,j=-1;i<b.size();i++)//因为i=0已赋值,因此从i=1开始循环;由于实现过程中j需要加1,因此初始值赋为第一个下标减1,即-1
    
        while(j>-1 && b[i]!=b[j+1])//若j返回初始值也停止循环
            j=next[j];
        if(b[i]==b[j+1])
            j++;
        next[i]=j;
    

void calm()

    for(int i=0,j=-1;i<a.size();i++)
    
        while(j>-1 &&(j==b.size()-1 || a[i]!=b[j+1]))//因为下标从0开始,所以b的最后一个元素的下标为b的长度减1
            j=next[j];
        if(a[i]==b[j+1])
            j++;
        f[i]=j+1;//由于下标从0开始,因此下标会比实际位置少1,所以这里要加1
    

int main()

    cin>>a>>b;
    pre();
    calm();
    for(int i=0;i<a.size();i++)
        if(f[i]==b.size())
            cout<<i+2-b.size()<<endl;//这里输出的是b在a中出现的第一个字符的位置,由于下标从0开始,所以i要加1;原式为i+1-b.size()+1
    return 0;
 

习题:


2019.4.7 于福建省石狮市

以上是关于KMP算法详解的主要内容,如果未能解决你的问题,请参考以下文章

疫情封校在宿舍学习KMP算法详解(next数组详解)附例

详解KMP算法

动态规划之 KMP 算法详解

详解KMP算法

详解KMP算法

详解KMP算法