朴素模式匹配
朴素模式匹配的时间复杂度为O(m*n),其中m为主串的长度、n为模式串的长度。
原因:
例如主串为 s = “aabaabaaf” m = 9
模式串为 t = “aabaaf” n = 6
双指针i,j: i遍历主串,j遍历模式串
若i和j对应位置的字符相等,i++、j++;若不相等 i = i - j +1、j = 0;
例如:i = 0 、j = 0时,直到s和t都遍历到索引位置5处,‘b’ != ‘f’,j回溯到0,i = i - j + 1,重新继续遍历直至j>=t.size()(未找到匹配串)或找到匹配串。
时间复杂度:上例需要匹配 4 * 6 = 24次;((m - n) + 1) * n;
对于其他字符串,最好情况下,第一次遍历就找到匹配串O(1);最坏情况下,每次遍历到最后一位才发现不同,直到最后一次遍历才完全匹配,时间复杂度为O(m * n);
KMP模式匹配算法
由朴素模式匹配引出KMP模式匹配算法:继续使用主串 s = “aabaabaaf”,模式串 t = “aabaaf”
发现遍历到索引位置5时(‘b’ != ‘f’),j不必回溯到索引0,因为"aabaabaaf"中索引5之前的字符串"aabaa"与模式串对应位置"aabaa"完全相同,又发现模式串中存在前缀和后缀完全相同的部分"aa" = “aa”,所以j到’b’位置(索引为2)的位置继续开始遍历,i同样也不要回溯,正好匹配成功。
注:前缀指字符串中包含首字符的所有子串(不考虑尾字符);后缀指子符串中包含尾字符的所有子串(不考虑首字符)。
例如:"aabaaf"的前缀有:a、aa、aab、aabaa、aabaa;后缀有f、af、aaf、baaf、abaaf、abaaf;
前缀表
前缀表在KMP算法中就是作为next[],用来判断当前不匹配时,模式串的j指针回溯到何位置。
如何计算模式串的前缀表:最长长度相等前后缀
例如: t = “aabaaf”
‘a’, next[0] = 0;
“aa” , next[1] = 1;
“aab”, next[2] = 0;
“aaba”, next[3] = 1;
“aabaa”, next[4] = 2;
“aabaaf”, next[5] = 0;
结论:
t = “aabaaf”
next 010120
注:j回溯到next[j-1]的位置
代码求解next[]数组:
void getNext (int next[], string &t) {
//四步
//1、初始化
//i表示后缀末尾
//j表示前缀末尾、同时表示最长相等前后缀的长度
int j = 0;
next[0] = 0;
for (int i = 1; i < t.size(); ++i) {
//2、前后缀不相同
while(j > 0 && t[i] != t[j]) {
j = next[j - 1];
}
//3、前后缀相同
if(t[i] == t[j]){
++j;
}
//4、更新next[]
next[i] = j;
}
}
完整kmp算法:
#include <iostream>
#include <string>
using namespace std;
void getNext (int next[], string &t) {
//四步
//1、初始化
//i表示后缀末尾
//j表示前缀末尾、同时表示最长相等前后缀的长度
int j = 0;
next[0] = 0;
for (int i = 1; i < t.size(); ++i) {
//2、前后缀不相同
while(j > 0 && t[i] != t[j]) {
j = next[j - 1];
}
//3、前后缀相同
if(t[i] == t[j]){
++j;
}
//4、更新next[]
next[i] = j;
}
}
bool kmp(string &s, string &t) {
int i = 0, j = 0;
int next[t.size()];
getNext(next, t);
while (i < s.size() && j < t.size()) {
// j回溯到0时 i+1;
if (j == 0 || s[i] == t[j]) {
++i;
++j;
}else{
j = next[j - 1];
}
}
if ( i > s.size()) {
return false;
}
return true;
}
int main()
{
string s = "aabaabaaf";//主串
string t = "aabaaf"; //模式串
cout << kmp(s, t) << endl;
}