KMP是一个字符串匹配算法,对于原本的暴力朴素做法进行了优化,使得时间复杂度大大降低,它的名字是取三个发明人的名字缩写。
一、KMP算法基本概念与核心思想
基本概念:
- ①
s[ ]
是 模式串:较长字符串, - ②
p[ ]
是 模板串,较短字符串。 - ③ “非平凡前缀”:指 除了最后一个字符以外,一个字符串的 全部头部组合(前面连续的部分)
- ④ “非平凡后缀”:指 除了第一个字符以外,一个字符串的 全部尾部组合。(后面均简称为 前/后缀)
- ⑤ “部分匹配值”:前缀和后缀 的 最长共有元素 的 长度。
- ⑥
next[ ]
是“部分匹配值表”,即next
数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心。
核心思想:
在每次失配时,不是把p
串往后移一位,而是把p
串往后移动至下一次可以和前面部分匹配的位置,
这样就可以 跳过大多数的失配步骤。
每次p
串移动的步数通过查找next[ ]
数组确定的。
二、next数组的含义
含义:next[ j ]
表示p[ 1, j ]
串中前缀和后缀相同的最大长度(部分匹配值),
即:p[ 1, next[ j ] ] = p[ j - next[ j ] + 1, j ](前后缀相同,两者都达到最大)
举个例子,例如:
为了对next
数组有更清晰的认知,我们手动模拟一下next
数组
假设 模板串 p = “abcab”,
则对于next[1]
:前缀集合为空,后缀集合为空,next[1] = 0
;
next[2]
:前缀集合 { “a” },后缀集合 { “b” },两集合中无匹配字符串,next[2] = 0
;
next[3]
:前缀集合 { “a”, “ab” },后缀集合 { “c”, “bc” },两集合中无匹配字符串,next[3] = 0
;
next[4]
:前缀集合 { “a”, “ab”, “abc” },后缀集合 { “a”, “ca”, “bca” },两集合中最长匹配字符串为 “a”,next[4] = 1
;
next[5]
:前缀集合 { “a”, “ab”, “abc”, “abca” },后缀集合 { “b”, “ab”, “cab”, “bcab” },两集合中最长匹配字符串为 “ab”,next[5] = 2
;
(注意以上说的前后缀都指的是“非平凡”)
得到以下表格:
三、匹配的思路
KMP算法主要分 两步:求next
数组、匹配字符串。
对于匹配字符串,其思路是这样子的:
模式s
串 和 模板p
串都人为规定为 从1
开始。
i
从1
开始,j
从0
开始,每次将s[ i ]
和p[ j + 1 ]
比较。
下方的图中,红色的字符串代表模式s
串,蓝色和紫色的串代表匹配过程中后移的模板p
串,
当匹配过程到上图所示时,
s[ a , b ] = p[ 1, j ] && s[ i ] != p[ j + 1 ]
此时要移动p
串(不只移动1
格,而是直接移动到下次能匹配的位置)
其中上图中的 ①串 为[ 1, next[ j ] ]
,③串 为[ j - next[ j ] + 1 , j ]
。由匹配可知 ①串 等于 ③串,③串 等于 ②串。所以 直接移动p
串,使 ① 到 ③ 的位置 即可。这个操作可由 j = next[ j ]
直接完成。 如此往复下去,当 j == m
时匹配成功(此时就能够完全匹配上了:p
串最后一个元素p[m]
与s
相配了)。
匹配过程的代码片段:
for(int i=1, j=0; i<=n; ++i)
{
while(j && s[i]!=p[j+1]) j = ne[j];
//如果j有对应p串的元素, 且s[i] != p[j+1], 则失配, 移动p串
//用while是由于移动后可能仍然失配,所以要继续移动直到匹配或整个p串移到后面(j = 0)
if(s[i]==p[j+1]) ++j;
//当前元素匹配,j移向p串下一位
if(j==m)//满足匹配条件
{
//匹配成功,进行相关操作
j = ne[j];//继续匹配下一个子串
}
}
四、求next数组
next
数组的求法是通过 模板串 自己与自己 进行匹配操作得出来的(代码和匹配操作几乎一样)。
始终记住一点: next[ ]
是“部分匹配值表”,即next
数组,它存储的是字符串中(一般是模板串p
中)每一个下标对应的“部分匹配值”,是KMP算法的核心。
代码和匹配操作的代码几乎一样,关键在于:每次移动 i
前,将 i
前面已经匹配的长度记录到next
数组中。
代码片段如下:
void get_next()//核心是求模式串p的next数组(记住next数组是相对于模式串而言的)
{
for(int i=2, j=0; i<=m; ++i)//i从2开始,因为ne[1]=0,无需计算
{
while(j && p[i]!=p[j+1]) j = ne[j];
if(p[i]==p[j+1]) ++j;//此时匹配到了前后缀相等
ne[i] = j;//赶紧记录下来
}
}
五、时间复杂度
O ( n + m ) O(n+m) O(n+m)
六、完整代码实现
空白代码:
#include<bits/stdc++.h>
using namespace std;
const int M = 1e5+10, N = 1e6+10;
int m, n;
char p[M], s[N];
int ne[M];
void get_next()
{
for(int i=2, j=0; i<=m; ++i)
{
while(j && p[i]!=p[j+1]) j = ne[j];
if(p[i]==p[j+1]) ++j;
ne[i] = j;
}
}
int main()
{
cin>>m>>p+1>>n>>s+1;
get_next();
for(int i=1, j=0; i<=n; ++i)
{
while(j && s[i]!=p[j+1]) j = ne[j];
if(s[i]==p[j+1]) ++j;
if(j==m)
{
printf("%d ", i-m);
j = ne[j];
}
}
return 0;
}
加注释:
#include<bits/stdc++.h>
using namespace std;
const int M = 1e5+10, N = 1e6+10;//M为模板串长度,N为模式串长度
int m, n;
char p[M], s[N];//p为模板串,s为模式串
int ne[M]; //next[]数组,这样取名避免和头文件next冲突
void get_next()//求next[]数组
{
for(int i=2, j=0; i<=m; ++i)
{
while(j && p[i]!=p[j+1]) j = ne[j];
if(p[i]==p[j+1]) ++j;
ne[i] = j;
}
}
int main()
{
cin>>m>>p+1>>n>>s+1;//下标从1开始
get_next();//求next[]数组
//匹配操作
for(int i=1, j=0; i<=n; ++i)
{
while(j && s[i]!=p[j+1]) j = ne[j];
if(s[i]==p[j+1]) ++j;
if(j==m)//满足匹配条件
{
//匹配完成后的具体操作
//如:输出以0开始的匹配子串的首字母下标
printf("%d ", i-m);//若从1开始,加1
j = ne[j];//继续匹配
}
}
return 0;
}
参考资料:AcWing 831. KMP字符串