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 于福建省石狮市

posted on 2019-08-20 17:51  TEoS  阅读(326)  评论(0编辑  收藏  举报