前置:
尝试去思考这样一个问题:给定字符串S和P,询问在字符串S中,字符串P出现了几次?
设S = "aabcdedf", P = "abc";
我们先从最暴力的方法入手,不难想到去针对S的每一位去和P暴力匹配。
当 $i = 0$ 时, 字符串匹配如下图:
$s[0] == p[0]$ 则继续向后匹配,发现 $s[1] != p[1] $,则执行 $i++$ 进行下一次匹配。
当 $i = 1$ 时,匹配如下图
$s[0] == p[0]$ 继续向后匹配, 直到 $s[i + strlen(p) - 1] == p[strlen(p) - 1]$ 证明匹配成功。
令 $ans++$,并且 $i++$ 进行下一次匹配。
按上述方法匹配直到字符串S被遍历一遍,输出 $ans$ 即可。
代码:

1 int fun(char *s, char *p){
2 int ans = 0;
3 for (int i = 0; i < strlen(s); ++i){
4 int j = 0;
5 for (; j < strlen(p); ++j){
6 if (s[i + j] == p[j])
7 continue;
8 else
9 break;
10 }
11 if (j == strlen(p))
12 ++ans;
13 }
14 return ans;
15 }
设字符串S的长度为 $n$ , P的长度为 $m$ ,不难计算出该暴力算法的复杂度为 $O(nm)$,在数据量较大的时候必定超时。
KMP算法:
KMP算法则是对上述过程中的一个优化,使得在每次匹配的过程中,当遇到失配的情况时,可以通过之前已经匹配的信息优化匹配过程,而不用每次都从字符串P的起始位置重新匹配。
这个优化的过程就是通过 $next$ 数组实现的。 我们将字符串S称为文本串,将字符串P成为模式串。
KMP算法分为两个阶段:1、求模式串的 $next$ 数组。 2、结合 $next$ 数组进行匹配。
$next$ 数组:$next[i]$ 的含义为:模式串第 $i$ 位与文本串适配时,下一次匹配应该从 模式串第 $next[i]$ 个位置开始匹配。
图例:

上图匹配过程中在 $p = 4$ 时发生失配,结合 $next$ 数组此时我们下一次匹配的起始位置应是:

因为右移四位后,模式串中又会有一个“AB”与文本串匹配。
不难得出结论: $next[i]$ 为 $i$ 之前的模式串的最长公共前后缀的长度。
之后去求 $next$ 数组的过程其实是模拟串自我匹配的过程

int nxt[1005];
void get_next(char *s){
int lens = strlen(s);
int i = 0, j = -1;
nxt[0] = -1;//默认nxt[0] = -1
while (i < lens){
//如果j = -1 或者当前两位相同,则最长公共前后缀可往后扩展。
if (j == -1 || s[i] == s[j]){
++i;
++j;
nxt[i] = j;//记录当前位置最长公共前后缀的长度。
}
//失配则i不变, j需移动到nxt[j]的位置继续匹配
else{
j = nxt[j];
}
}
}
在求得 $next$ 数组之后,我们便可以根据 $next$ 数组进行匹配。

int kmp(char *s, char *p){
//s为文本串, p为模式串
int lens = strlen(s), lenp = strlen(p);
int ans = 0;
int i = 0, j = 0;
while (i < lens){
//若j = -1或者匹配成功则继续匹配
if (j == -1 || s[i] == p[j]){
++i;
++j;
}
//失配则i不变, j移动到nxt[j]位置继续匹配
else{
j = nxt[j];
}
//匹配成功
if (j == lenp)
++ans;
}
return ans;
}
以上便是KMP算法的大体内容,复杂度为 $O(n + m)$。
