我眼中的KMP
1. KMP可以用来解决什么问题
KMP主要应用在字符串匹配上。KMP的主要思想是:当模式串与主串出现不匹配的字符时,可以知道一部分已经匹配的文本内容,可以利用这个避免重头开始匹配,大大的节省了时间。
2. KMP怎么用
KMP算法核心在于next数组的求解,但在求解前后缀、最长公共前后缀。下面分别对这三个概念加以阐述:
1. 前缀和后缀
假设你的字符串是 aabaaf
啥叫前缀呢?前缀就是带有第一个字符但不包括最后一个字符的所有顺序子串。那么前缀就是:a
,aa
,aab
,aaba
,aabaa
;
啥叫后缀呢?后缀就是带有最后一个字符但不包括第一个字符的所有顺序子串。那么后缀就是:
f
,af
,aaf
,baaf
,abaaf
。
2. 最长公共前后缀
最长公共前后缀就是:从所有子串的首位开始,比较当前子串相同前后缀的最大长度。还是以上面字符串为例:aabaaf
,
子串 | 最长公共前后缀 |
---|---|
a | 0 |
aa | 1 |
aab | 0 |
aaba | 1 |
aabaa | 2 |
aabaaf | 0 |
事实上,最长公共前后缀就是前缀表、next数组等。
next数组的作用:回退!!!它记录了模式串和主串不匹配时,模式串应该从哪里重新开始匹配。
3. next数组
next数组就是前缀表,对应到子串中,可以表示如下:
a | aa | aab | aaba | aabaa | aabaaf |
---|---|---|---|---|---|
0 | 1 | 0 | 1 | 2 | 0 |
即next数组为:int* next = {0, 1, 0, 1, 2, 0};
4. 举例
假设主串为 aabaabaaf
,模式串为 aabaaf
,当匹配的时候,发现匹配到f的时候发现匹配不成功,然后就去找next数组,就要找当前字符的前一个next数组的数字,我们可见匹配不成功的是f,找f前一个next数组的数字下标,一查表,发现是2,所以就回退到第2个字符处重新匹配,可见,就从b重新开始匹配;从b开始匹配之后,发现就成功匹配了所有模式串。
3. 用C++编写代码
class KMP{
public:
void GetNext(int* next, const string& s){
int j = 0;
next[0] = j;
for(int i = 1; i < s.size(); ++i){
while(j > 0 && s[i] != s[j]){
j = next[j - 1];
}
if(s[i] == s[j])
j++;
next[i] = j;
}
}
int strStr(string& mainStr, string& patternStr){
if(patternStr.size() == 0)
return -1;
int next[pattern.size()];
GetNext(next, patternStr);
int j = 0;
for(int i = 0; i < mainStr.size(); ++i){
while(j > 0 && mainStr[i] patternStr[j])
j = next[j - 1];
if(mainStr[i] == patternStr[j])
++j;
if(j == patternStr.size())
return i - patternStr.size() + 1; // 如果匹配成功,res返回的是模式串在主串中首次匹配的位置。
}
return -1;
}
};
int main(){
string mainStr = "aabaabaaf";
string patternStr = "aabaabaaf";
KMP s;
int res;
res = s.strStr(mainStr, patternStr);
cout << res << endl;
return 0;
}
4. 时间复杂度
假设主串长度 m,模式串长度 n:
如果暴力匹配字符串的话,时间复杂度是 O(m * n);
如果使用KMP算法的话,时间复杂度是 O(m + n)。