KMP算法详解
前置:
尝试去思考这样一个问题:给定字符串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 = 6$ 时发生失配,结合 $next$ 数组此时我们下一次匹配的起始位置应是:
因为右移四位后,模式串中又会有一个“AB”与文本串匹配。从而不用 $i$ 移动。
不难得出结论: $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)$。