前言
KMP算法最近看了很多的博客和视频始终没有清楚理解NEXT数组的构建,在B站看到正月点灯笼大神的视频,里面提出的前缀表对我有很大启发,今天来记录分享一下。
BILIBILI 正月点灯笼
定义
string text;//目标字符串
string pattern; //匹配字符串
int i,j;//为两个字符串索引 text[i],pattern[j]
int n,m;//分别为两个字符串长度
算法
KMP框架
在灯神的视频里引入了一个前缀表(Prefix table)的概念来替代Next数组,我觉得更容易理解,在这里前缀的意思我就不赘述了,可以自行百度。
假设待匹配的字符串Pattern=“ABABAAC”。
0 A
0 AB
1 ABA
2 ABAB
3 ABABA
1 ABABAA
0 ABABAAC
于是我们可以根据每一个字母的前缀数建立一个前缀表,为了方便后面KMP搜索,将其向后移一位,第一位定义为-1,于是有了这样的一个数组
int prefix[] = {-1,0,0,1,2,3,1};
对应到数组为这样的形式
当我们遍历text字符串时,若发现第j位Pattern不匹配时候我们这样处理(KMP匹配就不赘述了)
如图,我们在pattern字符串第2位发现A和B不匹配,我们这时候查找前缀表数组,发现第2位对应着是0,这时则将pattern第0位移动到这里j = prefix[j] = 0
,如图。
如此往复,每次发现不匹配则将前缀表对应数字的下标的位置向后移动,如果移动到prefix[0]=-1,则将整个pattern向后移动一位(与第-1位移动到第0位一个意思)
按这样的模式匹配,当我们pattern遍历完成就可以确定一个匹配的数据,此时若我们还要继续往下匹配,我们将pattern移动到当前前缀表的数字处,即 j = prefix[j]
。于是我们就可以写出kmp匹配的代码
void kmp_search(string text, string pattern) {
int n = pattern.length(), m = text.length();
int i = 0, j = 0;
//prefix_table(pattern);
//move_prefix_table(n);
while (i < m) {
if (j == n - 1 && text[i] == pattern[j]) {
cout << "Find pattern at" << i - j << endl;
j = prefix[j];
}
if (text[i] == pattern[j]) i++, j++;
else {
j = prefix[j];
if (j == -1) i++, j++;
}
}
}
- 第一个判断为判断当前pattern是否匹配结束
- 第二个判断若相等i和j向后移动一位,若不相等如上述按前缀表移动,前缀表数值为-1时移动一位。
前缀表构建
至此,我们已经了解了怎么通过前缀表匹配,现在要来构造前缀表。若是通过暴力枚举,仍然要花费很大的时间复杂度构建前缀表,于是需要通过之前的数据和规律来构造
0 A
0 AB
1 ABA
2 ABAB
3 ABABA
1 ABABAA
0 ABABAAC
这里我们用整型变量len来表示当前最大前缀长度(这里指的是真前缀,长度小于字符串),用整型变量i来表示当前匹配的位置,如图第一位永远为0,于是len = 0
。对于接下来的每一个字符串我们只需要判断新增加的字符pattern[i]是否与pattern[len]相等,若相等则len++;这里仍然存在一个问题,如下图。
当我们插入第7位‘A’时,此时len=2,i=6,我们发现 pattern[len] != pattern[i]
,但是插入的‘A’与字符串开头相同,所以这种情况下len不能简单等于0,应该进行如下操作(灯神视频里讲的有些乱可以自己看视频理解下)。
void prefix_table(string pattern) {
prefix[0] = 0;
int len = 0;
int i = 1;
while (i < pattern.length()) {
if (pattern[i] == pattern[len]) {
len++;
prefix[i] = len;
i++;
}
else {
if (len > 0) len = prefix[len - 1];
else prefix[i++] = len;
}
}
}
这样我们就完成了前缀表的构建,但记得之前还提到过需要将前缀表向前移动一位利于KMP搜索,于是还需如下函数(在函数中无法动态获取数组大小,于是当做参数传入)。
void move_prefix_table(int n) {
int i;
for (int i = n; i > 0; i--) prefix[i] = prefix[i - 1];
prefix[0] = -1;
}
于是整合全部功能就完成了KMP搜索。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
int prefix[N];
void prefix_table(string pattern) {
prefix[0] = 0;
int len = 0;
int i = 1;
while (i < pattern.length()) {
if (pattern[i] == pattern[len]) {
len++;
prefix[i] = len;
i++;
}
else {
if (len > 0) len = prefix[len - 1];
else prefix[i++] = len;
}
}
}
//向前移一位,prefix[0] = -1
void move_prefix_table(int n) {
int i;
for (int i = n; i > 0; i--) prefix[i] = prefix[i - 1];
prefix[0] = -1;
}
void kmp_search(string text, string pattern) {
int n = pattern.length(), m = text.length();
int i = 0, j = 0;
prefix_table(pattern);
move_prefix_table(n);
while (i < m) {
if (j == n - 1 && text[i] == pattern[j]) {
cout << "Find pattern at" << i - j << endl;
j = prefix[j];
}
if (text[i] == pattern[j]) i++, j++;
else {
j = prefix[j];
if (j == -1) i++, j++;
}
}
}
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
string text,pattern;
cin >> text >>pattern;
kmp_search(text,pattern);
return 0;
}