KMP算法是一种高效的字符串匹配算法,相比于BF算法的时间复杂度为O(n*m),它的时间复杂度降低到了O(n+m)。这种算法的高效性在于它利用了主串的指针不回溯,而只移动模式串的指针位置。然而,对于初学者来说,KMP算法并不容易理解。因此,本文将采用问题导向的方法,以通俗易懂的方式解释KMP算法。
(注:本文主串与模式串字符的默认起始位置为1!)
我们先感性地匹配一下如下两个字符串(x表示未知字符):
主串: xxxxaaabaaabxxxx
模式串: aaabaaad
此时主串的指针i 指向b,模式串的指针j指向d,两者此时“失配”了。根据BF算法的匹配方式,此时指针i将回溯到绿色的a,指针j回溯到第一个a.此时细心的同学可以发现,其实并不需要如此“大打出手”,因为回溯依旧无法匹配。其实,指针i可以保持不动,指针j只移动b位置上,让模式串头三个字符aaa直接匹配主串字符b前的三个字符aaa,此时才作后续的判断,匹配效率必定会更高一点.
那么问题来了,此种匹配方式真的可信吗?如果真的可行,那么每次“失配”,指针j移动到什么位置呢?这是下文所需解决的问题.
我们需要确定指针j前的子串的最长相同前后缀的长度len,那么j的回溯位置就是len+1.
在开始如下内容前,务必搞清楚如下几个概念:
1.前缀:包含首位字符但不包含末位字符的子串.
2.后缀:包含末位字符但不包含首位字符的子串.
3.字符串的最长相同前后缀:
若字符串为:abacab,那么它的前缀为:{"a","ab","aba","abac","abaca"},后缀为:{"b","ab","cab","acab","bacab"},显然最长相同前后缀为:"ab".
为了方便解释原理,命名模式串指针j前的子串为sonString,sonString的最长相同前缀为:preString,最长相同后缀为:sufString.
如上图所示,长条代表主串,短条代表模式串,此时h无法与g匹配,在红色长条中也存在最长相同的前后缀"ba"。此时为了提高匹配的效率,指针i保持不动,指针j指向a,如下图所示:
很显然,当指针j所指向的字符出现“失配”的情况下,通过让最长前缀preString覆盖住最长后缀fudString,能够更高效地匹配模式串。同时,若preString的长度为len,那么指针j应该回溯到len+1.
剩下的问题就是,如何求指针j的回溯位置了.
定义next数组,next[j]表示:当模式串的指针j所指向的字符与主串“失配”时,指针j应该回溯的位置,即next[j]代表j前面的子串的最长相同前后缀长度+1.
若next[i] = k,那么它有如下意义:
字符串:
其中子串:
与
,前者为最长相同前缀,后者为最长相同后缀.
给定模式串便能求算next数组,代码实现如下:
#include
using namespace std;
const int N = 100;
int Next[N];
char mStr[N];
char tStr[N];
void GetNext(char ch[], int length) {
Next[1] = 0;
int i = 1, j = 0;
while (i <= length) {
if (j == 0 || ch[j] == ch[i]) {
Next[++i] = ++j;
}
else {
j = Next[j]; // 模式串的指针回溯位置
}
}
}
求算next数组的代码很简短,它的原理十分奥秘,以下是它的推导过程:
根据代码所示,我们规定Next[1] = 0,指针i指向sonString的最长相同后缀后面的第一个字符,指针j指向最长相同前缀后面的第一个字符,很显然,当ch[i]==ch[j]时,指针i+1后面的子串的最长相同前后缀的长度就是j+1.
当ch[i]!=ch[j]时,假设k1 = Next[j],k2 = Next[k1],那么公式合理性推导如下:
原sonString为:
,此时
,但是
.
因为k1 = Next[j],根据对称性,a1....a(k1-1) == a(j+1-k1)....a(j-1)==a(i+1-k1)....a(i-1),因在下一轮循环中,如果a[j]==a[i](此时j==k1)],那么a1....a(k1)==a(i+1-k1).....a(i),显然成立.
若a[k1]!=a[i],那么重复j = Next[j],推导方式同上.
#include
using namespace std;
const int N = 100;
int Next[N];
char mStr[N];
char tStr[N];
void GetNext(char ch[], int length) {
Next[1] = 0;
int i = 1, j = 0;
while (i <= length) {
if (j == 0 || ch[j] == ch[i]) {
Next[++i] = ++j;
}
else {
j = Next[j]; // 模式串的指针回溯
}
}
}
int stringMatch() {
int mLen = strlen(mStr + 1), tLen = strlen(tStr + 1);
int i = 1, j = 1;
while (i <= mLen && j <= tLen && i - j <= mLen - tLen) {
if (mStr[i] == tStr[j]) {
i++, j++;
}
else {
j = Next[j];
}
if (j == 0) {
j++, i++; // 重新开始匹配
}
}
if (j == tLen + 1) {
return i - tLen;
}
else {
return 0;
}
}
int main() {
while (1) {
memset(Next, 0, sizeof(Next));
cout << "输入主串与模式串:" << endl;
cin >> mStr + 1;
cin >> tStr + 1;
GetNext(tStr, sizeof(tStr + 1));
int pos = stringMatch();
if (pos) {
cout << "匹配成功,主串的匹配起始位置为:" << pos << endl;
}
else {
cout << "匹配失败..." << endl;
}
}
return 0;
}