KMP算法解析与实现

最近学了KMP算法,学了半天终于学会了,感觉还是挺难理解的,在这里与大家分享一下我的思考,也方便我自己复习。
下面就正式进入讲解环节吧(本文所有字符串初始坐标为0!)。


首先我们要明确KMP算法是干什么的:我们有两个字符串T与S,现在我们要找到T中为S的子串,求出满足条件子串数量及位置。这时候我们就要用到KMP算法。


传统算法回顾:

在讲解KMP算法前,我们先看一下传统方式的查询。
在字符串T与S中各设一个指针i,j=1;依次比对i+1,j+1、i+2,j+2……..
如果发现有不同的,则比对i+1,j=1;
用图来看一下(加粗为当前比对位置):
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
然后
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
然后
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
这时我们发现c3与d3不同,就比对T-c2与S-c1,即:
T: c1 c2 c3 c4 c5
S: __ c1 c2 d3 e4
这样的比对,扫描T需要O(n)时间,检查比对需要O(n)时间。
总时间复杂度:O( n^2 ),这样的复杂度还算快的。
然而与KMP相比,还是太慢了,KMP可以把时间复杂度优化到O(n+m);
下面让我们来具体看看KMP的实现。


运用next数组实现跳转:

先引入两个概念:
- 前缀串:例如abcde,则其前缀依次为:a,ab,abc,abcd,abcde
- 后缀串:例如abcde,则其后缀串依次为e,de,cde,bcde,abcde
对于ababa这个目标串(对比串),我们可以发现:
其前缀串与后缀串有相同部分!这就是KMP实现的关键。
比对abacdabe与ababa时,我们可以看到:
T: a b a c d a b e
S: a b a a b a
有第三位置<初始坐标为0>上的a与c不同。
当我们将S往后移动时由于第一位与第三位相同,前两位已经比对过了。
所以后移后绝对不会匹配.
这样就浪费了大量的时间。
而KMP的比对就会直接跳到:
T: a b a c d a b e
S:——a b a a b a
具体是如何实现的呢?我们引入一个F[i]来记录目标串S的前缀串与后缀串的最大相同量。
例如:a b a a b a
则对于未知i,有:
i=1,a,F[1]=无;
i=2,a b,F[2]=无;
i=3,a b a,F[3]=1;
i=4,a b a a,F[4]=1;
i=5,a b a a b,F[5]=2;
i=6,a b a a b a,F[6]=3;
但为了方便计数(等会你就知道为什么了),也方便区分无相同前缀的位置,我们将所有F减1,即无相同前缀的为-1;
所以:
F[1]=-1;F[2]=-1;F[3]=0;F[4]=0;F[5]=-1;F[6]=2;
下文中所有的表示都是这样的。
然后我们用几个例子看一看next是如何实现跳转的。
例如匹配abaaddddd与abaaba,则用前缀数组的方法为:
前缀数组:a -b a a -b a(‘-‘方便对齐)
———— -1 -1 0 0 1 2
匹配到:
T: a b a a d d d d d
S: a b a a b a
F[5-1]=0,有一个相同的前缀,j直接跳转到F[j-1]+1=1(注意初始下标为0);即:
T: a b a a d d d d d
S: ——–a b a a b a
此时匹配位置前一个的’a’已经匹配,实现跳转。再看一个:
匹配abaabdddd与abaaba,匹配到:
T: a b a a b d d d d
S: a b a a b a
此时F[6-1]=1,有1+1=2个相同前缀,j跳转到F[j-1]+1=2,即:
T: a b a a b d d d d
S: ——–a b a a b a
读者们可以仔细体会一下上面两个例子,这里我给出跳转的总结:

if(t[i]!=s[j])
{
   if(j==0)i++;    //没有相同前缀,直接向后移(与一般方法相同)
   else j=F[j-1]+1;    //跳转,KMP
}

求解next数组:

知道如何用next数组实现跳转后,我们还要想办法实现next数组的求解。
对于next数组,我们求解的方法与上文其实差不多,但是用到了一个思想:自己匹配自己!
我们先思考:求解next数组肯定要用O(m)的算法吧。(要保证KMP的时间复杂度)。
这里的思路是一个递推思路。
结合下面的代码读者们不妨先想一想,什么样的情况下F[i]可以由前面的F[k]加1求得。
先给出求解F数组的代码:

void get_low()        
{
    len=t.length();
    F[0]=-1;    
    int temp=0,i;
    for(i=1;i<=len-1;i++)      //初始下标为0!
    {
        int j=F[i-1];          
        while(t[j+1]!=t[i]&&j>=0)j=F[j];                
        if(t[j+1]==t[i])F[i]=j+1;  
        else F[i]=-1;
    }return;
}

还是先从例子开始吧。
所求前缀数组:
S: a _b a a b _b a b a a b
F: -1 -1 0 0 1 -1 0 1 2 3 4

我们求解第二个:
S:a __b a a b
F:-1 -1
此时这个a的前一个b的F值为-1,所以此时的a不能由b的F[1]求出,j=-1;
比对S[j+1]=a,与当前位a相同,所以F[i]=F[2]=j+1=0;

我们求解第五个:
S= a b a a b b
F=-1 -1 0 0
此时j=F[i-1]=F[4]=0,但当前位置b与前一个a不同,无法接上,j=F[j]=0;
然后比对S[j+1]=S[2]=’b’与当前位b相同,F[i]=F[5]=j+1=1;

这一部分虽然代码很短,但这是KMP的难点,比较难理解(但一定不能背代 码),希望读者们进行一次完整的模拟以助于理解(对照上文的代码),这里给出一个典型的例子以供参考:
–典型例子:S: a _b a a b _b a b a a b //下标线为方便对齐
–对应next:F:-1 -1 0 0 1 -1 0 1 2 3 4
这个例子把next的所有情况都包含到了,希望读者们一定要耐着性子模拟一遍。


理解了以上内容后(我承认有点难度),KMP算法的实现应该也就不难了,下面我给出完整的代码<内含用next匹配的具体代码>:

模板题来自于:洛谷 P3375 【模板】KMP字符串匹配

题目描述

如题,给出两个字符串s1和s2,其中s2为s1的子串,求出s2在s1中所有出现的位置。

为了减少骗分的情况,接下来还要输出子串的前缀数组next。

(如果你不知道这是什么意思也不要问,去百度搜[kmp算法]学习一下就知道了。)
输入输出格式
输入格式:

第一行为一个字符串,即为s1(仅包含大写字母)

第二行为一个字符串,即为s2(仅包含大写字母)

输出格式:

若干行,每行包含一个整数,表示s2在s1中出现的位置

接下来1行,包括length(s2)个整数,表示前缀数组next[i]的值。

输入输出样例
输入样例#1:

ABABABC
ABA

输出样例#1:

1
3
0 0 1

说明

时空限制:1000ms,128M
设s1长度为N,s2长度为M
对于100%的数据:N<=1000000,M<=1000

完整代码(即KMP算法的模板):

#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

char t[10000002],s[10002];  //原串、模式串
int F[10002];     //前缀next数组
int len,len1;

inline void get_bef()   //求解next前缀数组
{
    int i,j;
    F[0]=-1;len=strlen(s);
    for(i=1;i<=len-1;i++)
    {
        j=F[i-1];
        while(j>=0 && s[i]!=s[j+1])j=F[j];
        if(s[i]!=s[j+1])F[i]=-1;
        else F[i]=j+1;
    }return;
}

inline void find_pos()  //求解位置
{
    int i,j;
    len1=strlen(t);j=0;i=0;
    while(i<=len1-1)
    {
        if(t[i]==s[j])              //这里为正常匹配
        {
            i++;
            j++;
            if(j==len)printf("%d\n",i-j+1);
        }
        else                        //KMP跳转
        {
            if(j==0)i++;
            else j=F[j-1]+1;           
        }
    }return; 
}

int main()
{
    scanf("%s",&t);
    scanf("%s",&s);
    get_bef();
    find_pos();
    for(int i=0;i<=len-1;i++)printf("%d ",F[i]+1);
    return 0;
 }

希望我的一点理解感悟能够帮助到oier们,呵呵呵,谢谢观看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值