KMP算法简介
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。
一般的字符串匹配算法算法在子串中有多个字符和模板串中的若干个连续字符比较都相等,但最后一个字符比较不相等时,模板串和子串的比较位置都需要回退,而KMP算法在上述情况下,模板串位置不需要回退,从而可以大大提高效率
KMP算法的核心是利用子串匹配失败后的信息,尽量减少子串与模板串的匹配次数以达到快速匹配的目的,具体实现就是在匹配过程中维护一个next数组,数组中包含了子串的匹配信息,匹配失败后通过next数组的信息将子串回溯到指定位置从新匹配。
KMP算法的时间复杂度O(m+n) 。
一般字符串匹配算法
图片来源: 阮一峰的网络日志
1.首先,模板字符串"BBC ABCDAB ABCDABCDABDE"的第一个字符与搜索词"ABCDABD"的第一个字符,进行比较。因为B与A不匹配,所以搜索词后移一位。
2.因为B与A不匹配,搜索词再往后移。
3.就这样,直到字符串有一个字符,与搜索词的第一个字符相同为止。
4.接着比较字符串和搜索词的下一个字符,还是相同。
5.直到字符串有一个字符,与搜索词对应的字符不相同为止。
6.这时,最自然的反应是,将搜索词回溯到起始位置,模板字符串移动到这次失败的比较开始时的下一个位置,再从头逐个比较。
7.这样做虽然可行,但是效率很差,因为要把模板串移到已经比较过的位置,重比一遍
KMP算法原理
KMP算法的核心是利用子串匹配失败后的信息,尽量减少子串与模板串的匹配次数以达到快速匹配的目的,具体实现就是在匹配过程中维护一个next数组,数组中包含了子串的匹配信息,匹配失败后通过next数组的信息将子串回溯到指定位置从新匹配。
具体来说就是(还是上面那个例子)
当遇到失配情况时,我们可以看到在失配之前,子串与模板串的每个元素都是匹配的,那也就是说,如果子串失配元素的前一个元素或前多个元素与子串首元素或首几个元素相同,只需要将子串回溯到这个相同首元素位置,那么模板串就可以接着继续和子串匹配了
总结一下KMP算法就是:失配时将子串回溯到与失配元素上一个或多个元素相同的开始位置元素位置
要想随时找到一个可以替换的元素将子串回溯的话,就需要维护一个数组,起名为next,next数组中用于存放子串回溯的位置,即失配时子串应该回溯到该索引(下标)值指定的位置,初始时我们将其首元素置为-1其他元素置0
因为next数组存放的是子串回溯的索引值,即子串数组的下标,初始时,首元素以外的元素应该置0,表示失配时回溯到子串的起始位置,首元素置-1,表示失配时回溯到子串首元素的前一个元素
每次匹配的时候我们都可以知道匹配元素的值,这时我们就可以将该元素与首元素比较,如果相等,那么就在next数组记录下首元素的索引
当然,没这么简单,还有一种情况是前面的匹配过程已经改变了next数组的值(next数组已经不为0),这时就不能简单的与首元素比较了而是比较当前元素前一个元素对应next数组位置的元素值,若两个元素值相同,则将其下一个元素索引(当前元素前一个元素对应next数组位置的元素下一个位置索引)存入对应next数组中
这样是因为会出现当子串的尾部元素与首部元素连续匹配的情况,如“ABCDABC”,尾部元素“ABC”与首元素连续相等
此时子串的逻辑结构为
维护next数组后,当子串与模板串失配时,只需要将子串回溯到失配元素前一个元素对应的next数组元素值位置即可
代码实现
#include<iostream>
#include<string>
using namespace std;
//kmp算法思路就是模板串只遍历一次,每次不匹配时回溯子串,若子串中后部元素匹配成功且有与开始部位相等的元素时,回溯时只需将开始部分回溯到当前位置(因为与开始部分相同所以相同部分依旧与模板匹配)
int kmpAlgorithm(string pattern,string mate){
int next[mate.length()]={-1};//将next数组初始化,第一个元素为-1,其余元素均为0,next数组元素用于确定回溯子串时的回溯位置
int mateIndex=0; //该变量为当前匹配的子串索引
//该循环用于遍历待匹配的模板字符串,每次循环会比较一个子串元素和模板元素,循环时模板串不会回溯,回溯的只有子串
for(int i=0;i<pattern.length();++i){
if(pattern[i]==mate[mateIndex]){//当模板串与子串元素匹配
if(mateIndex>=mate.length()-1)//子串与模板串匹配成功
return 1;
if(mateIndex>0){//当mateIndex不等于0时(并非next第一个元素),将当前结果放置到next数组中
if(next[mateIndex-1]>=0){//判断当前元素前一个next数组数据是否不为0,即子串当前元素在开始位置是否连续重复出现
if(mate[next[mateIndex-1]+1]==mate[mateIndex]){//判断与连续出现的后一个是否相同
next[mateIndex]=next[mateIndex-1]+1;//相同则记录该子串元素索引以便后续回溯
mateIndex++;
continue;
}else{
next[mateIndex]=0;//不相同则置为0后续回溯到子串头位置重新匹配
mateIndex++;
continue;
}
}else{//当前元素前一个next数组数据是否为0,判断是否与首元素相等
if(mate[0]==mate[mateIndex]) {//当前元素与首元素相等,记录索引
next[mateIndex]=1;
mateIndex++;
continue;
}else{
next[mateIndex]=0;//不相同则置为0后续回溯到子串头位置重新匹配
mateIndex++;
continue;
}
}
}else{//当 mateIndex等于0,即next数组第一个,置为-1
next[mateIndex]=-1;
mateIndex++;
continue;
}
}else{//模板与子串元素匹配不成功,回溯子串
mateIndex=next[mateIndex-1];
next[mateIndex]=0;
mateIndex++;
continue;
}
}
return 0;
}
int main(){
string pattern="abcdabcfhabcdcabcabc";
string mate1="abcabc";
string mate2="abcabd";
cout<<"pattern:"<<pattern<<endl;
cout<<"mate1:"<<mate1<<endl;
cout<<"mate2:"<<mate2<<endl;
cout<<"kmpAlgorithm(pattern and mate1):"<<kmpAlgorithm(pattern,mate1)<<endl;
cout<<"kmpAlgorithm(pattern and mate2):"<<kmpAlgorithm(pattern,mate2)<<endl;
return 0;
}