C++数据结构难点-kmp算法 详细图解

kmp算法用于解决寻找母字符串S的子串P

以下为原题,也是kmp能解决的经典问题5ae62a7f389847e2b0f53c639e009420.png

代码如下:(无注释)

#include<iostream>

using namespace std;

int const N = 1e5+10,M = 1e6+10;
int n,m;
char s[M],p[N];
int ne[N];

int main()
{
    cin>>n>>p+1>>m>>s+1;

    //计算next数组
    for (int i = 2,j = 0;i<=n;i++)
    {
        while(j && p[i] != p[j+1]) j = ne[j];
        if (p[i] == p[j+1]) j++;
        ne[i] = j;
    }


    //匹配kmp
    for(int i = 1,j = 0;i<=m;i++)
    {
        while(j && p[j+1] != s[i]) j = ne[j];
        if (p[j+1] == s[i]) j++;
        //if (j == 0),continue;这时候直接让i进一位,所以不写
        if (j == n)
        {
            printf("%d ",i-n+1-1);
            j = ne[j];
        }
    }
}

有注释:

#include<iostream>

using namespace std;

int const N = 1e5+10,M = 1e6+10;
int n,m;
char s[M],p[N];
int ne[N];

int main()
{
    cin>>n>>p+1>>m>>s+1; //这里把两个字符串全部输入进去,下标从1开始
    
    //求next过程
    for(int i = 2,j = 0;i <= n;i++) //这里自动有ne[1]=0,ne[2]=0
    {
        while(j && p[i] != p[j+1]) j = ne[j];
        //循环退出有两种情况
        if (p[i] == p[j+1]) j++;
        //if (j == 0),直接i++,所以不需要写任何东西
        ne[i] = j;
    }

    //kmp匹配过程
    for(int i = 1,j = 0;i <= m;i++)
    {
        //只要前方一个数不相同,就一直退
        while(j && p[j+1] != s[i]) j = ne[j];

        //循环退出有两种可能,一是j退无可退(j = 0),二是不再满足p[j+1] != s[i]
        //其中j = 0 的情况不需要写任何代码:if (j == 0),直接i++,不需要写任何东西
        if (p[j+1] == s[i]) j++;

        if (j == n) //匹配成功
        {
            printf("%d ",i-n+1-1);//本题要求输出下标从0开始,故-1
            //匹配成功后,j若再+1就一定不匹配了,所以直接令j = ne[j]寻找下一个可能匹配的字符串
            j = ne[j]; 
        }
    }

    return 0;
}

暴搜做法

#include<iostream>

using namespace std;

int main()
{
    for(int i = 1;i<=m;i++)
    {
        bool flag = true;
        for(int j = 1;j <= n;j++)
        {
            if(p[j] != s[i+j-1])
            {
                bool flag = false;
                break;
            }
            if(flag) printf("%d",i);
        }
    }
}

可以看到,代码在不断地循环计算,复杂度为O(m*n),不可接受

 

优化做法

d5588df79f87405e95a229886f3c812a.jpeg

先看第一幅图,当前母串s的蓝色部分可以和模版串p的一些部分匹配,但是从某一位开始匹配不上

如果我们不想使用暴力搜索,那就要思考一个问题:绿色模版串至少向前前进几位才有可能再次匹配上?

我们如果让绿色模版串前进p位后,有三种可能

1.前进后一位都匹配不上,直接pass

2.前进后能匹配上一些位数,如第二幅图,但是这样是无效的,尽管有一些段是匹配的,但只要有一位不匹配,那整个串就不可能匹配成功

3.第三幅图的情况,绿色模版串向前移动一定位数后, 除了上次匹配失败的那一位,其他所有位数都可以匹配成功,我们要找的就是这种情况

 

然而这时候会出现一个问题:

Q1:绿色模版串向前移动p位,可能有多个p可以使绿色串移动后是图3的情况,那么我们需要哪一种呢?

答案是我们要找到最小的p,因为题目的输出要求是:每当模版串成功匹配时,输出母串中成功匹配部分的第一个数的下标。只有我们使用最小的p才保证不会漏解。

至于这个最小的p如何寻找,就是kmp的核心,next数组

kmp的核心:next数组

我们先定义一下前缀和后缀,假设现在有一个字符串“abababababa”(编号从1开始)

长度为3的前缀就是aba,长度为4的前缀就是abab

也就是:从第一个字母开始长度为某一个数的字符串

再来看后缀,这个字符串的第4为是b,那第四位b的一个长度为2的后缀就是ab,同理,第5位的长度为3的后缀就是aba

第i位的长度为j的后缀就是:从i-j+1到i的字符串

 

我们接下来再来定义next数组:对于一个字符串的第i位数,next[i]就是最大的使得第i位的前缀后缀相等的数(且长度不能大于或等于i,这个先记下来,不要细究原因),比如上文提到的abababababa字符串,i = 5时,next[5] = 3,因为最大可以取3,使得第五位的前后缀相同

 

next和p的关系

5bb78b4a1c0f4c1b99755174c4c7b9dd.jpeg

假设我们现在已经知道了这条绿色模版串的next数组(即对每一位i,都知道next[i]是多少),那每当我们要移动绿色串的时候,要移动的最小位数p应该是多少呢?

d5588df79f87405e95a229886f3c812a.jpeg

答案是p = i - next[i],移动i - next[i]位

举个例子:(注意这里上方的是母串,下方位模版串,这里的i和上文的i不同)

不要管为什么是i-1和j对应,i和j+1对应,这个不重要,只是一种习惯

5768689b6f4443e9b306374110afa666.jpeg

现在我们正在尝试匹配模板串的第j+1位和母串的第i位,c和b匹配失败,这里j = 3,j+1 = 4

下一次可能让模版串匹配成功的情况,需要将模版串向前移动2位,也就是j - next[j](j是3,next[j]是1)

以上是为了理解模版串的移动,实际写代码中我们可以直接让j = next[j],达到同样的效果

 

正向看一遍

1b37df5701d343bfb9408d41391c0997.jpeg

现在母串的蓝色部分和模版串的前一半绿色部分是成功匹配的,但是母串的红圈位置与模版串的绿圈位置的字母不匹配,我们这时候应该怎么做?

答案是向前移动j - ne[j]位

ps:这里ne就是next数组,取名为ne是为了防止一些头文件冲突

 

什么是移动?

我们接下来要把图上字符串的移动转变为代码

现在在母串s上有一个指针i,i会遍历整个母串,对于每个i,我们要将s[i]与p[j+1]比较,绿色模版串的“移动”实际上就是改变“比较的是第几位”,因此直接令 j = ne[j]就可以达到移动j - ne[j]的效果

 

这里举一个匹配的例子,以帮助大家更好地理解kmp的匹配过程

983ba7c34e494722bca00d860f2fb323.jpeg

只看蓝色的字,不要管上面绿色的线段

    //kmp匹配过程
    for(int i = 1,j = 0;i <= m;i++)
    {
        //只要前方一个数不相同,就一直退
        while(j && p[j+1] != s[i]) j = ne[j];

        //循环退出有两种可能,一是j退无可退(j = 0),二是不再满足p[j+1] != s[i]
        //其中j = 0 的情况不需要写任何代码:if (j == 0),直接i++,不需要写任何东西
        if (p[j+1] == s[i]) j++;

        if (j == n) //匹配成功
        {
            printf("%d ",i-n+1-1);//本题要求输出下标从0开始,故-1
            //匹配成功后,j若再+1就一定不匹配了,所以直接令j = ne[j]寻找下一个可能匹配的字符串
            j = ne[j]; 
        }
    }

结合代码理解:

第一步,可以看到s[i]与p[j+1]匹配成功,那么i与j都向前移动一位

第二步,s[i]与p[j+1]匹配失败,则令j = ne[j],ne[j] = 4,移动6-4=2位,也就是j = ne[j] 

如果一直j = ne[j]且始终匹配不成功,则j最终会等于0,同样跳出while循环,此时只需要让i前进1位即可

如果j = n了,说明p的每一位全部匹配成功,输出匹配成功的字符串的首位数下标,也就是i-n+1,但是本题要求下标从0开始,那就再-1即可,无伤大雅

这里注意一旦j匹配成功,那么j后面就已经没有数字了,为了寻找下一个可能匹配的数,应该令j = ne[j]

next数组的计算

next数组的计算不需要手动遍历,可以用递推取得,方式与kmp匹配的过程非常类似,就是用模版串p自己匹配自己

这里与母串s完全无关,模版串p每一位i的ne[i]仅与p自己有关,因为这里在找p中每一位的最大公共前后缀

f7d08a82edb64804b85880d0a7545bcd.jpeg先明确目标和手段,我们要求第i位的最大公共前后缀,我们采用递推的方法,所以已知1~i-1位的ne[]数组(类似第二类数学归纳法)

如果蓝色部分已经匹配成功,我们想知道第i位的最大公共前后缀ne[i],是否需要重复求解呢?

答案是不需要,我们只需要比较p[i]与p[j+1],如果相同,则i与j都向后移动一位,记录ne[i] = j即可

相应的,如果p[i]!= p[j+1],只需令j = ne[j]比较下一个可能匹配成功的数即可

结合代码理解:

    //求next过程
    for(int i = 2,j = 0;i <= n;i++) //这里自动有ne[1]=0,ne[2]=0
    {
        while(j && p[i] != p[j+1]) j = ne[j];
        //循环退出有两种情况
        if (p[i] == p[j+1]) j++;
        //if (j == 0),直接i++,所以不需要写任何东西
        ne[i] = j;
    }

如果j不是0,就比较p[i]与p[j+1],成功匹配则退出循环,j++,记录ne[i] = j

匹配失败则令j = ne[j],再次尝试匹配,不断循环

如果循环一直失败,直到j = 0,j会最后匹配一次,如果成功,j++,ne[i] = 1,如果又失败,那说明这一位的最大公共前后缀是0,直接i++即可

(因为我们的ne[]数组是设置在main函数外,所以不操作默认是0,直接i++跳过即可)

 

以下再举个例子帮助理解

10a366abb3f24cc0be768bc25de11111.jpeg

以此这样遍历一遍就可以得到p数组中每一位的ne值

 

复杂度分析

可以看到,指针i只进不退,j指针虽然指的是模版串p,但是图中可以看到,j指针指向母串s的位置同样也是只进不退,因此复杂度O(m+n),远远优于暴力搜索的O(m*n)

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值