基础算法——单模字符串匹配KMP

本文详细介绍KMP算法的核心概念——部分匹配表next[]及其构建过程,通过实例解释如何利用next[]加速字符串匹配,节省不必要的比较步骤。

题目描述

给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串P在模式串S中多次作为子串出现。

求出模板串P在模式串S中所有出现的位置的起始下标。

输入描述
第一行输入整数N,表示字符串P的长度。

第二行输入字符串P。

第三行输入整数M,表示字符串S的长度。

第四行输入字符串S。

数据范围:

1≤N≤1041≤N≤10^41N104

1≤M≤1051≤M≤10^51M105

输出描述
共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。

输入样例

3
aba
5
ababa

输出样例

0 2

算法思想

KMP算法的核心是一个被称为部分匹配表的数组:next[]。例如对于字符串abababca,为了方便处理,第一个字符从位置1开始,那么它的各部分如下:

字符abababca
位置i12345678
next[i]00123401

就像上图中所示,如果模式字符串(待匹配的字符串)有8个字符,那么next[]数组就会有8个值。

字符串的前缀和后缀

如果字符串S1S_1S1S2S_2S2,存在S1=S2XS_1=S_2XS1=S2X,其中XXX是任意的非空字符串,那就称S2S_2S2S1S_1S1前缀

例如,HarryHarryHarry的前缀包括{H,Ha,Har,Harr}\{H, Ha, Har, Harr\}{H,Ha,Har,Harr},我们把所有前缀组成的集合,称为字符串的前缀集合。

同样可以定义后缀S1=XS2S_1=XS_2S1=XS2, 其中XXX是任意的非空字符串,那就称S2S_2S2S1S_1S1的后缀。

例如,PotterPotterPotter的后缀包括{otter,tter,ter,er,r}\{otter, tter, ter, er, r\}{otter,tter,ter,er,r},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的前缀或后缀

next[]数组的意义

next[]表示的是字符串的前缀集合与后缀集合的交集中最长元素的长度

例如,对于ABAABAABA,它的前缀集合为{A,AB}\{A, AB\}{A,AB},后缀集合为{A,BA}\{A, BA\}{A,BA},两个集合的交集为{A}\{A\}{A},那么前缀集合与后缀集合的交集中最长元素的长度为1

再比如,对于字符串ABABAABABAABABA,它的前缀集合为{A,AB,ABA,ABAB}\{A, AB, ABA, ABAB\}{A,AB,ABA,ABAB},它的后缀集合为{A,BA,ABA,BABA}\{A, BA, ABA, BABA\}{A,BA,ABA,BABA}, 两个集合的交集为{A,ABA}\{A, ABA\}{A,ABA},其中最长的元素为ABAABAABA,长度为3。

查找匹配

在源字符串S="ababababca"中查找模式字符串P="abababca",如下图所示:
在这里插入图片描述
如果在源串i处与模式串j+1处的字符不匹配,即s[i] != p[j + 1],说明源字符串中i之前的next[j]位字符就一定与模式字符串的第1~j位是相同的,即源串在第i位失配,那么S[i−j...i−1]=P[1...j]S[i - j...i - 1] = P[1...j]S[ij...i1]=P[1...j]

而上面表格中记录了当j = 6时,ne[j] = 4,在图1.12 (a)中就是ababababababababab,其前缀与后缀集合的最长元素为abababababab,长度为4。那么就可以断言,源字符串中i指针之前的 4 位一定与模式字符串的第1位至第 4 位是相同的,即长度为 4 的后缀与前缀相同。

这样一来,就可以将这些字符段的比较省略掉。具体的做法是,保持i指针不动,然后将j指针指向模式字符串的ne[j]位即可,如图1.12 (b)所示。

简言之,以图中的例子来说,在 i 处失配,那么源字符串和模式字符串的前边6位就是相同的。又因为模式字符串的前6位,它的前4位前缀和后4位后缀是相同的,所以推知源字符串i之前的4位和模式字符串开头的4位是相同的。就是图中的灰色部分。那这部分就不用再比较了。

重复这个过程,如果查找过程中,在源串中找到和模式串长度相同的子串,那么就找到一个匹配字符串。

构造next[]

next[]的过程完全可以看成字符串匹配的过程,即以模式字符串为源字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。

具体来说,就是从模式字符串的第2位(注意,next[1] = 0)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

时间复杂度

在匹配过程中,源字符串指针i只向前移动、不回溯,所以时间复杂跟源字符串的长度nnn相关,近似于O(n)O(n)O(n)

代码实现


#include <iostream>

using namespace std;

const int N = 100010, M = 1000010;

char p[N], s[M];

int n, m, ne[N];

int main()
{
    cin>> n >> p + 1 >> m >> s + 1; //模式串和文本串都从1开始

    //求ne[]
    ne[1] = 0; //初始状态,第一个字母匹配失败就只能从0开始重新匹配
    for(int i = 2, j = 0; i <= n; i ++)
    {
        //j没有回到起点,并且没有匹配成功
        while(j && p[j + 1] != p[i]) j = ne[j];
        
        if(p[j + 1] == p[i]) j++; //匹配成功
        
        ne[i] = j; //将当前匹配个数赋值给ne[i]
    }

    //kmp匹配
    for(int i = 1, j = 0; i <= m; i ++)
    {
        //j没有回到起点(即没有重新开始匹配),并且模式串的下一个字符跟文本串当前字符匹配失败
        while(j && p[j + 1] != s[i]) 
            j = ne[j]; //回溯模式串的指针j,从ne[j]开始继续匹配,相当于将模式串后移
        
        if(p[j + 1] == s[i]) //匹配成功
            j++;//模式串指针j后移,继续匹配下一个字符
       
        if(j == n)
        {
            //模式串匹配完毕
            cout << i - n << ' '; //输出匹配起点位置,位置从0开始
            
            j = ne[j];//移动模式串指针j,继续下一次匹配
        } 
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值