本篇总结的是关于串中的KMP
算法解析
应用场景
现给定两个串,现在要看较短的一个串是不是较长的串的子串,如果是就输出子串后面的内容,如果不是则输出Not Found
能匹配到:
长串:qwertabcde
短串:abcd
则可以在长串中找到短串的内容,则输出abcde
匹配不到:
长串:qwertabcde
短串:afcd
则无法在长串中匹配到短串的内容,则输出Not Found
算法方案
对于如何匹配串的问题,首先是一种暴力的方案,例如让短串的内容不断地和长串进行匹配,如果在短串和长串中对应到了,就两个同时向后移动,如果短串到头,就说明匹配成功了,如果遇到其他字符,就重新进行匹配,这就是暴力求解的方案,但是时间复杂度高,总体来说是一个O(MN)
的时间复杂度
这样的时间复杂度对于算法来说是比较高的,于是有三个大佬Knuth
、Morris
、Pratt
,发明了一个著名的字符串匹配算法,因此这个算法的名字就被命名为KMP
算法
算法原理
为了方便叙述,定义str
是这里的长串,pattern
是要匹配的串
算法原理就是创建一个next
数组,里面存储的是pattern
中,下标为i的字符前的字符串最长相等前后缀的长度
那什么是最长相等前后缀?用下面的例子来举例:
假设现在pattern
是abcab
,那么对于pattern
来说,它的前后缀分别有:
前缀:{a,ab,abc,abca,abcab}
后缀:{b,ab,cab,bcab,abcab}
因此对于pattern
来说,它的next
数组可以这么表示
从pattern
的最后一个字符来看,它的前面的字符串是abca
,而对于这个串来说的相等的前后缀只有a
这一个,因此这里填入的就是a
的长度也就是1
但是这个数组有什么用?从下面这个例子来看:
假设现在str
为abcabeabcabcmn
,pattern
为abcabcmn
那么写出pattern
的next
数组:
下面就开始进行匹配了,当匹配到e
和c
的时候匹配失败了,此时如果是暴力算法的思路来看,需要让pattern
和str
的第二个字符开始对齐,再重新匹配,但是对于KMP
算法来说,next
数组的作用就出现了
只需要让不匹配的字符下标对应的next下标的值,回溯到pattern下标即可
以上面的例子为例,现在是s[5]
和p[5]
的匹配失败了,那么next[5]
对应的数据是2
,那么就意味着现在要让s[5]
和p[2]
进行对齐匹配,也就是说,设匹配失败的字符下标为i
,那么就要让s[i]
和p[next[i]]
进行匹配
这样就是一个循环了,进行多次循环即可,这也就是KMP
算法的核心所在
next数组的意义:
- 下标为
i
的字符前的字符串最长相等前后缀的长度 - 该处字符不匹配时应该回溯到的字符的下标
上面的next
数组写法只是手算出来的,在实际算法中需要将next
数组用代码实现写出来:
void GetNext(const string& pattern, vector<int>& next) { int i = 0, j = -1; next[0] = -1; while (i < pattern.size() - 1) { if (j == -1 || pattern[i] == pattern[j]) { next[++i] = ++j; } else { j = next[j]; } } }
对于上面的代码来进行解析:
- 如果两个i和j的对应的字符相等,那么i和j就同步向后移动
- 如果不相等,就要进行回退了,回退到它们原来最长公共前后缀的地方,
i
指向的是后面的最长公共前后缀,j
回退到前面的最长公共前后缀,如果这两个前后缀相等,那么这就组成了一个新的最长相等前后缀,就可以进行数据的写入了
关于求出next
数组后,利用这个数组求KMP
算法的代码:
int KMP(const string& str, const string& pattern, const vector<int>& next) { int i = 0, j = 0; while (i < (int)str.size() && j < (int)pattern.size()) { if (j == -1 || str[i] == pattern[j]) { i++, j++; } else { j = next[j]; } } if (j == pattern.size()) { return i - j; } else { return -1; } }
在知道next
数组后,解决剩下的问题就很容易了,只需要一一进行比对,如果不满足条件就进行回溯,如果走到头就返回下标,如果不满足条件就返回-1
完整代码
#include <bits/stdc++.h> using namespace std; // KMP算法,给定两个字符串,用子串去匹配长字符串,如果匹配成功就输出匹配的字符串和后面的内容 // 如果匹配不成功就输出NOT FOUND void GetNext(const string& pattern, vector<int>& next) { int i = 0, j = -1; next[i] = j; while (i < pattern.size() - 1) { if (j == -1 || pattern[i] == pattern[j]) { next[++i] = ++j; } else { j = next[j]; } } } int KMP(const string& str, const string& pattern, const vector<int>& next) { int i = 0, j = 0; while (i < (int)str.size() && j < (int)pattern.size()) { if (j == -1 || str[i] == pattern[j]) { i++, j++; } else { j = next[j]; } } if (j == pattern.size()) { return i - j; } else { return -1; } } void PrintString(const string& str, int index) { string res; for (int i = index; i < str.size(); i++) { res += str[i]; } cout << res << endl; } int main() { // str是长字符串,pattern是要匹配的子串 string str, pattern; cin >> str >> pattern; // KMP算法首先计算出pattern的next数组 vector<int> next(pattern.size()); GetNext(pattern, next); // 根据str,pattern,next数组进行匹配 int index = KMP(str, pattern, next); // 得出结果 if (index == -1) { cout << "NOT FOUND" << endl; } else { PrintString(str, index); } return 0; }