KMP算法
先注明,本文章转载于自己的CSDN博客
KMP算法用于字符匹配工作
朴素匹配方法的复杂度是O(N*M)
KMP算法复杂度达到了O(N+M)
从这表达式来说,复杂度明显地降低了
但这个算法最大的问题,就是很难理解。 在网上看了很多人,包括有个哥大家都说他讲得已经是最好的那个了,但是我还是没有看明白他在说什么,只是大概了解了他的思路。
回去之后,翻了下数据结构和算法的书,认认真真地看了遍,收获很大。 特在这做一下笔记,方便大家一起学习。
KMP算法的意思就是(这里采用我在很多书上看到的理解之后的一个汇总版) 在那之前,我们要明白两个东西
1.目标串:就是我们要比对的串(就是那个往往都会比模式串更长的那个)
2.模式串:就是那个检查在目标串中是否匹配的那个串
在KMP算法中,模式给我们提供了一个工具,通过这个工具,使得我们减少了时间,这个工具就是next函数(这个就是KMP算法中最难理解的一部分)。
一般来说,我们在比较两个串的时候,都是采用先比较各个位置,要是不对,就将指向模式串的指针复原,即下一次检查就从模式串的开头来看。 然后指向目标串的指针向后移动一个位置。
但是KMP算法告诉我们,其实在移动时候,我们不必要傻傻地移动一位,而移动的位数,我们可以通过我们要比对的对象知道。 而把这个移动的位数,或者说是下一个要比对的对象下标记下来,那么之后遇到在这个点不匹配的时候,我就直接移动到对应的位置,而不需要只移动一位,这样就节省了时间。
而这个储存,我们就是采用next实现的,不管你之前是看了多少其他的说法,请把之前对于next函数的看法放到一边,再认真看我下面的文字
这里采用的是,next[j] = k 表示,第j个位置的模式串和对应的目标串中的字符不匹配的时候,就用第k位的,模式串中字符和目标串的失配的位置进行比较
next函数不仅仅表示了位置,同时也描述了长度,这点在后面的文字中慢慢理解,这里先讲述出来
看到这里,你可以理解了KMP算法的大致操作了。如果没有,请再看一遍上面的文字,一个被称之为难的算法,只看一遍就能懂,那真是很高的传授技术加上双方的好运了。
next函数的生成,可以说是最难理解的一部分,但是,我相信,这也只是临门一脚的问题。 因为,next函数的生成就是按照KMP算法本身来产生的。 试想一下,next函数不正是在同一个串内的字符串的比较么? 为了理解这个,我废了很大功夫
我做了这样的一个假设: 两个空字符一定相等。这一点非常重要
那么用递推公式给出next 如果前面的已经匹配好了。(先不管匹配了有多长,是从哪开始了) 但是我们保证了前面的那个一定是匹配好的。
我们可以做上面的假设的原因就是我们之前讲到的那个空字符一定是相等的,那么我就可以说这对空字符是匹配好的
因为最小,我们都可以说有一对空字符相等,所以,就可以做上面的假设。
那么就开始匹配了。我们在这里恰好使用以模式串开头部分作为了我们所说的模式串。(这就是前缀)
将next[0]设置为-1。 意思是,如果这个不匹配,就用next[-1]来和这个开头的不匹配的字符来进行匹配。 但是next[-1]是不存在是,在我的这里,就假设为了那个空字符串。
所以,就是将这个最开头那个空的(不存在的)那个字符,匹配当前的串,不就是将整个串向后移动一位(至少对于第一个串中不匹配的对象是这样的)。
到这里,关于为什么next[0]要取-1要清楚了
之后呢,往下走。如果这个位置都匹配的,那么下面向下移动的之后的那个位置(假设匹配不对的位置),每一个串是前面那个字符的匹配情况加一。本质上是双方本来就往后移动了,那么就直接赋值就好了。
如果不匹配,那么由于我们采用的是前面是模式串,后面是目标串的抽象定义。就是说,前面那些字符的next值已经算好了的,那么就直接用前面算好的next值进行回溯。 用之前KMP的算法的思路,就实现了所有模式串的next值。 next值生成代码如下:
void getNext(int next[]) {
int j = 0, k = -1, length = str.size();
// 其中k表示的是前面的那个串,而j表示的是后面那个串,用同一个串的不同起始点的串进行比较
// 前面的那个串去匹配后面的那个串,通过这个方式来进行匹配(也是一个KMP)
next[0] = -1; // 表示如果开头不能匹配,就用这个前面那个-1来匹配,也就是后移动一位
while(j < length) {
if (k == -1 || str[j] == str[k]){
j ++; k ++;
next[j] = k;
} else {
k = next[k];
}
}
}
看完上面的文字,就要把KMP算法看懂了,之后再看hiho1015
和一般的KMP不同,这一题,要求对于匹配出来的串进行计数。
就是看究竟匹配了几次?
对于原来的KMP的改的地方就是
if (k == str.size()) {
ans += 1;
k = next[k];
}
加了上这个函数地方。
这里的意思是,如果匹配完成了,就将计数数字加一,并且,将原来的进行回溯,如果这个回溯理解不了,那么是因为前面的KMP还没有看懂,要再看一次,就可以明白,这里保证不管对不对,在这个串的位置,一定是不匹配的(你的长度都超过了还能对???),那就一定要回溯。
文末有原题链接
代码如下:
#include <iostream>
using namespace std;
string str, ptr;
void getNext(int next[]) {
int j = 0, k = -1, length = str.size();
// 其中k表示的是前面的那个串,而j表示的是后面那个串,用同一个串的不同起始点的串进行比较
// 前面的那个串去匹配后面的那个串,通过这个方式来进行匹配(也是一个KMP)
next[0] = -1; // 表示如果开头不能匹配,就用这个前面那个-1来匹配,也就是后移动一位
while(j < length) {
if (k == -1 || str[j] == str[k]){
j ++; k ++;
next[j] = k;
} else {
k = next[k];
}
}
}
int KMP_Ans(int next[]) {
int j = 0, k = 0, length = ptr.size(), ans = 0;
while (j < length) {
if (k == -1 || str[k] == ptr[j]) {
j ++; k ++;
if (k == str.size()) {
ans += 1;
k = next[k];
}
} else {
k = next[k];
}
}
return ans;
}
int main(){
int time;
cin >> time;
while (time--){
cin >> str>> ptr;
int *a = new int [str.size() + 1];// 将数组开稍微大点
getNext(a);
cout << KMP_Ans(a)<< endl;
delete[] a;
}
}
hihocoder原题链接
但要登录好了之后,才能直接跳转到题目,否则就是只有登录链接,不过可以登录完了之后,再来点击这个,也就可以直接访问了。
CSDN原文链接
有彩色字,可能会更好看一点。
欢迎关注我的公众号:肥宅Sean笔记
肥宅Sean笔记