前段时间被问了字符串匹配的问题,用的是暴力,一直想填KMP的坑,这篇文章就记录一下,方便你我他,希望能讲清楚。
概念
本篇文章的一些约束说明,可能与其他文章不一样,但是原理一样的~
-
前缀:对于一个字符串
s
,从第一个字符开始的子串。 -
后缀:对于一个字符串
s
,以最后一个字符结尾的子串。 -
最长相同前后缀 :对于一个字符串
s
,其前缀子串与后缀子串相同,且长度最大的子串。比如字符串ababaaba
,有a
与aba
都是其相同前后缀,而aba
是其最长相同前后缀。 -
最长相同前后缀数组:对于一个字符串
s
,其所有从索引0
开始的子串的最长相同前后缀长度组成的数组,本文统称为L
数组。比如字符串ababaaba
,其数组为L = {0, 0, 1, 2, 3, 1, 2, 3}
,L[i]
表示从0
到i
的子串的最长相同前后缀长度,有下表:子串 最长相同前后缀长度 a 0 ab 0 aba 1 abab 2 ababa 3 ababaa 1 ababaab 2 ababaaba 3
L数组应用
这里我们先说L
数组,也就是最长相同前后缀数组有什么用吧,然后再说怎么获得一个字符串的L
数组。
普通匹配
这里先举个普通匹配的例子吧:从字符串A = ababaabbababaaba
中查找B = ababaaba
。
Step0
|ababaab|bababaaba
|ababaab|a
Step1
a|babaabbababaaba
|ababaaba
Step2
ab|aba|abbababaaba
|aba|baaba
Step3
aba|baabbababaaba
|ababaaba
Step4
abab|a|abbababaaba
|a|babaaba
Step5
ababa|ab|bababaaba
|ab|abaaba
Step6
ababaa|bbababaaba
|ababaaba
Step7
ababaab|bababaaba
|ababaaba
Step8
ababaabb|ababaaba|
|ababaaba|
普通匹配就是一个字符一个字符比较,前面的匹配结果对后面丝毫没有影响,这是个极大的浪费。我们在Step0
的匹配都快成功了,只差一个字符,然后又从A
的下一个字符重新开始匹配了。而用KMP算法就不一样了。
KMP匹配
同样的例子:从字符串A = ababaabbababaaba
中查找B = ababaaba
。
Step0
|ababaab|bababaaba
|ababaab|a
Step5
ababa|ab|bababaaba
|ab|abaaba
Step7
ababaab|bababaaba
|ababaaba
Step8
ababaabb|ababaaba|
|ababaaba|
使用KMP算法只需要上面几步。为什么KMP可以直接从Step0
跳到Step5
,再跳到Step7
呢?因为KMP算法充分利用了Step0
时候匹配到的结果。
Step0
匹配结束时的状态为:A
字符串的索引为i=7
,B
字符串的索引为j=7
,匹配到的子串是ababaab
。- 对于字符串
B = ababaaba
,其L
数组前面有说过,是L = {0, 0, 1, 2, 1, 1, 2, 3}
,则其子串ababaab
的最长相同前后缀长度为L[j-1] = 2
。 - 由于
A[i]
与B[j]
的字符不匹配,而L[j-1]=2
,则将j赋值为L[j-1]
,此时的状态为:A
字符串的索引为i=7
,B
字符串的索引为j=2
,匹配到的子串是ab
,即Step5
结束时的状态。 - 而后同理从
Step5
跳到Step7
。
这就是L
数组,也就是最长相同前后缀数组的作用。通过它,我们可以在匹配失败时,通过L
数组,查询与后缀相同的前缀所处的位置,直接重置j
的值。
KMP算法代码
L数组的求值
字符串s
的最长相同前后缀数组L
求值如下:
vector<int> getL(string s) {
unsigned long len = s.size();
vector<int> L(len, -1);
for (int i = 1; i < len; i++) {
// 对于子串[0,i-1],第i个字符
int j = L[i - 1];
// 若子串[0,i-1]存在相同前后缀,找到前缀的最后一个字符的索引
while ((j >= 0) && (s[j + 1] != s[i])) j = L[j];
// 判断能否扩展相同前后缀
if (s[j + 1] == s[i]) L[i] = j + 1;
else L[i] = -1;
}
return L;
}
初始化为-1
,表示不存在最长相同前后缀,也是为了方便判断第0
位与第i
位。
而当L[i]
的值不为-1
时,表示的是若子串substr(0, i+1)
存在相同前后缀子串时,前缀子串的最后一个字符在字符串s
中的索引。
那么在KMP算法中,其长度其实是L[i]+1
。
KMP匹配
字符串的kmp匹配算法,就是在普通匹配的基础上,将j
的遍历优化了,其优化原理与求L
数组原理一样。
只不过要注意:子串[0, j-1]的最长相同前后缀长度是L[j-1]+1
。
// 返回第一个匹配到的子串的首字符索引,若没有则返回-1
int kmp(string A, string B) {
int lenA = A.size(), lenB = B.size();
assert(lenA > lenB);
vector<int> L = getL(B);
int i = 0, j = 0;
while (i < lenA) {
if (A[i] == B[j]) {
i++; j++;
if (j == lenB) return i - lenB;
} else {
if (j == 0) i++;
// 如果A[i]与B[j]不相等,则将j赋值为子串[0,j-1]的最长前后缀的前缀长度
else j = L[j - 1] + 1;
}
}
return -1;
}