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

题目描述

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

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

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

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

第二行输入字符串P。

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

第四行输入字符串S。

数据范围:

1 ≤ N ≤ 1 0 4 1≤N≤10^4 1N104

1 ≤ M ≤ 1 0 5 1≤M≤10^5 1M105

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

输入样例

3
aba
5
ababa

输出样例

0 2

算法思想

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

字符 a b a b a b c a
位置i 1 2 3 4 5 6 7 8
next[i] 0 0 1 2 3 4 0 1

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

字符串的前缀和后缀

如果字符串 S 1 S_1 S1 S 2 S_2 S2,存在 S 1 = S 2 X S_1=S_2X S1=S2X,其中 X X X是任意的非空字符串,那就称 S 2 S_2 S2 S 1 S_1 S1前缀

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

同样可以定义后缀 S 1 = X S 2 S_1=XS_2 S1=XS2, 其中 X X X是任意的非空字符串,那就称 S 2 S_2 S2 S 1 S_1 S1的后缀。

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

next[]数组的意义

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

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

再比如,对于字符串 A B A B A ABABA ABABA,它的前缀集合为 { A , A B , A B A , A B A B } \{A, AB, ABA, ABAB\} { A,AB,ABA,ABAB},它的后缀集合为 { A , B A , A B A , B A B A } \{A, BA, ABA, BABA\} { A,BA,ABA,BABA}, 两个集合的交集为 { A , A B A } \{A, ABA\} { A,ABA},其中最长的元素为 A B A ABA ABA,长度为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)中就是 a b a b a b ababab ababab,其前缀与后缀集合的最长元素为 a b a b abab abab,长度为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只向前移动、不回溯,所以时间复杂跟源字符串的长度 n n n相关,近似于 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;
}

猜你喜欢

转载自blog.csdn.net/qiaoxinwei/article/details/108893140
今日推荐