题目链接 :点击查看
题目描述 :
给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。模板串 P 在模式串 S 中多次作为子串出现。求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
输入输出格式 :
输入
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
输入输出样例 :
输入
3
aba
5
ababa
输出
0 2
题目分析 :
要想了解KMP算法的优美与高效,应要与朴素字符串匹配算法(BF算法)相比较。若此题用BF算法来解,则需要嵌套两层for循环,第一个 for 中 i 表示主串中匹配到的位置,第二个 for 中 j 表示子串匹配到的位置,对于每一次 i , 二重循环中 将s[ i + j ] 与 p[ j ] 进行比较,如相等则将j ++ ,如不匹配则 j 回到p串开头,再将从主串s的下一位开始,如此进行比较匹配,总的时间复杂度为O(n * m)。而KMP算法时间复杂度则可以降到O(n + m),其核心做法是对BF算法中(指针) j 回溯进行改进,原本 s 与 p 不匹配直接回到开头,改进后 j 将回溯到 j 之前中最长相等前缀的位置,达到加速匹配的效果,同时在此过程中,主串的比较位置不需要回退。所以我们需要一个next数组来记录子串中每个点并以当前点为终点的区间,最长公共前后缀(最长相等前后缀) 。其中,构建next数组中的 j有两层含义: 1. 最大匹配长度 2 . 正在匹配中的前缀的末尾位置(下标)。而构建完next数组之后,next数组亦有两层含义:1. 最长公共前后缀 2. 最长前缀的末尾下标。关于构建next数组时如果前后缀相等的状况被打断时该怎么办?可以将 j 指针进行回退,回退到前一个状况,如仍存在有公共前后缀的情况,则再次比较p[i] 与 p[j] ,要还不匹配,则再次回退........直到 j 回到p开头。而后主串s与子串p的匹配过程与构建next数组做法大致相同,在比较s[i]与p[j]不相同时,对 j 进行回退,回退到上一个状态,即 j = ne[j - 1](注意在此j - 1是j的前一个点,要是s[i]与p[j]不匹配,我们当然要用j 的前一个点作为终点构成区间,寻找最长公共前后缀,而不是以当前点为终点),特别要注意,在s与p匹配成功之后,我们将j = ne[j - 1]回退到前一个状态,再向下进行匹配。详见如下代码。
代码 :
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e5 + 7;
int ne[N]; //ne数组记录每个位置最长公共前后缀,在发生移动时ne数组所记录的值亦可以充当当前位置最长公共前后缀前缀末尾的下标,即j要回退到的位置
int n, m;
string p, s;//s为主串(模式串), p为子串(模板串)
int main() {
cin >> n >> p >> m >> s;//从0开始
//第一个for循环是对next数组处理(可以理解为子串p自己与自己匹配), i指匹配位置,j指最大匹配长度,也指正在匹配的前缀的末尾
for (int i = 1, j = 0; i < n; i ++ ) {//当前点为j 和 i,若p[j] 与 p[i]不匹配, 需要看以上一个点j - 1为终点构成的区间
while (j && p[i] != p[j]) j = ne[j - 1];//当前缀与后缀的匹配中断时,j回退到上一个位置,并将p[i]与其再次进行比较,若还不相等,再次回退,直到回退到开头。
if (p[i] == p[j]) j ++; // 回退会导致当前最长公共前后缀的长度减少,也就是说,后面所需要比较的字符数目变多
ne[i] = j;
}
for (int i = 0, j = 0; i < m; i ++ ) {//i为主串s中所匹配到的位置,j在此与上面的含义不一样,为子串p中所匹配到的位置
while (j && s[i] != p[j]) j = ne[j - 1]; //j回退到上一个位置
if (s[i] == p[j]) {
j ++ ;
if (j == n) {//子串p的最大下标为j - 1,若 j == n则表明了上面if中的比较为主串与子串中的最后一个字符进行比较
cout << i - n + 1 << " ";//输出匹配完成的左端点
j = ne[j - 1]; //若匹配成功则回退到上个位置,再往下进行匹配。
}
}
}
return 0;
}
-------------------------------------------------------------------
下面我们给出KMP算法相应模板(下标从1开始)
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的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;
}
// 匹配
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)
{
j = ne[j];
// 匹配成功后的逻辑
}
}