KMP算法,又称模式匹配算法,能够在线性的时间内判定字符串B【1~M】是否为字符串A【1~N】的子串,并求出字符串B在字符串A中各次出现的位置和次数
一般来说,我们最常见也是最容易想到的就是暴力枚举字符串A中的每个位置i,把字符串B与字符串A的后缀A【i~N】对齐,向后扫描逐一比较B【1】与A【i】,B【2】与A【I+1】……是否相等。我们把这种比较的过程称为A与B尝试进行“匹配”。
上述的做法虽然常见,思路简单,但是时间复杂度是很高的,理想情况是O(N+M),但是很容易被卡成O(NM),且理想情况的概率是极小的,所以我们要学习KMP算法,KMP算法可以在O(N+M)的时间复杂度求出上述问题,首先记住一点就是,KMP算法是在移动模式串,而不是移动主串,也就是一直都是在移动B串
在我们的暴力做法中,可能会有很多重复操作,我们的优化就是减少重复操作来降低时间复杂度,例如:
A串为:abcdaefghiabc
B串为:abcde
在朴素算法中,我们首先从i==1开始往后逐一比较,然后发现到A[5]!=B[5](这里设字符的下标从1开始),就会回溯到i==2,然后再逐一比较,但其实这过程是重复且没用的,因为在i==1时,我们发现A[1]==B[1],A[2]==B[2],A[3]==B[3],A[4]==B[4],且B串中的前四个元素互不相同,那么就意味着B[1]和A的前四个元素也不可能相同,所以我们可以直接将B串往后移动4位,比较A【5】和B【1】,这样就等价于省去了i==2,i==3 ……开始往后遍历的情况,直接从i==5开始往后遍历,这样就省去了几步操作
再看第二种情况
A串为:bbsbbsasda
B串为:bbsbbc
首先从i ==1 开始往后遍历,在A[6]!=B[6],这个时候就不是把B串往后移动6位了,因为B[1]==B[4]==A[4],B[2]==B[5]==A[5],所以我们应该将B串往后移动4位,接着比较A[6]和B[3]
结合两种情况,我们发现移动多少位取决于B串中的前缀子串和后缀子串是否有相等的,且移动的是最大的公共前后缀子串,也就是说如果B串中有长度为3的前缀子串和后缀子串相等,也有长度为4的,我们把第4位的后面一位移到第一个不匹配的位置
我们用一个next数组存储B串中的以i结尾的非前缀子串与B的前缀能够匹配的最长长度,这样每次出现不匹配的时候就可以快速的知道移动几位,那么如何求next数组呢?
next数组是KMP算法的核心,求出next数组就可以得到我们想要的答案了
看一个例题
P3375 【模板】KMP 字符串匹配
题目链接:https://www.luogu.com.cn/problem/P3375
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1e6 + 10;
char s[N], s1[N];
int ne_s1[N];
int main()
{
scanf("%s%s", s+1, s1+1);
int s_len = strlen(s + 1);
int s1_len = strlen(s1 + 1);
ne_s1[1] = 0;
for (int i = 2, j = 0; i <= s1_len; i++)//求出模式串的next数组
{
while (j && s1[i] != s1[j + 1])j = ne_s1[j];
if (s1[i] == s1[j + 1])j++;
ne_s1[i] = j;
}
for (int i = 1, j = 0; i <= s_len; i++)//用模式串跟主串进行匹配
{
while (j && (j==s1_len||s[i] != s1[j + 1]))j = ne_s1[j];//要保证j大于0,等于0的时候等于将模式串移动到了第一位
if (s[i] == s1[j + 1])j++;
if (j == s1_len)//表示主串s中有一段区间与s1相等
printf("%d\n", i - s1_len + 1);
}
for (int i = 1; i <= s1_len; i++)
printf("%d ", ne_s1[i]);
return 0;
}