转自队友wwg博客:KMP算法
部分参考于大佬的博客:https://blog.csdn.net/starstar1992/article/details/54913261/
从头到尾彻底理解KMP:https://blog.csdn.net/v_july_v/article/details/7041827
一:单模字符串匹配问题与BF算法O(n*m):
(一)单模字符串匹配问题
所谓单模就是模式串只有一个,多模字符串匹配问题属于ac自动机部分,这里不做分析
(二)BF算法O(n*m)
两个指针,一个指针遍历文本串,另一个遍历模式串,每次遍历模板串后,文本串指针向前移动一个单位,时间复杂度为O(n*m)
#include<iostream>
#include<string>
using namespace std;
int BF(string ts, string ps){
int result = -1;
int ts_len = ts.size(),ps_len = ps.size();
int i,j;
for(i = 0, j = 0; i< ts_len - ps_len && j < ps_len;)
if(ts[i] == ps[j]) ++i,++j;
else i = i - j + 1,j = 0;
if(j == ps_len) result = i-j;
return result;
}
int main(){
string s = "ABCDABABGFABAC";
string p = "ABAB";
cout<<BF(s,p)<<endl;
return 0;
}
二:KMP算法所解决的问题:
kmp算法是解决单模字符串匹配问题的高效算法,利用next数组(失配函数)存储最长公共前缀后缀长度,优化BF算法,时间复杂度为 O(n+m)
三:算法描述与详解:
(一)前缀,真前缀,后缀,真后缀,最长公共前缀后缀长度
(1)前缀:字符串从0 ~ i ( i <= len )的所有子串,例如:”ABCDABD”的前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB,ABCDABD]
(2)真前缀:前缀除去本身,例如:”ABCDABD”的真前缀为[A, AB, ABC, ABCD, ABCDA, ABCDAB]
(3)后缀:字符串i ~ n( i >= 0 )的所有子串,例如:”ABCDABD”的后缀为[ABCDABD,BCDABD, CDABD, DABD, ABD, BD, D]
(4)真后缀:后缀除去本身,例如:”ABCDABD”的后缀为[BCDABD, CDABD, DABD, ABD, BD, D]
(5)最长公共前缀后缀长度(默认为真前缀后缀):前缀与后缀相同的子串的最大长度:
例如:”ABCDAB”的前缀为[A, AB, ABC, ABCD, ABCDA],后缀为 [BCDAB, CDAB, DAB, AB, B],共有元素为”AB”,最大公共 前缀后缀长度为2
(二)kmp算法描述:
在BF算法基础上,首先回顾BF算法,BF算法中每次一轮匹配结束,文本串的指针回溯到下一位,而实际情况下,会导致很多重复的 check,其实只需要回溯至最长公共前缀后缀处即可,如图所示,看图会更加清晰:
next数组返回当前的最长公共前后缀长度,假设为len。因为数组是由0开始的,所以next数组让第len位与主串匹配就是拿最长前缀之后 的第1位与失配位重新匹配
(三)算法可行性的描述:
四:算法实现:
(一)next数组的求解---kmp核心所在
void ca_next(string str, int len){
int k = -1;next[0] = -1;//-1表示不存在最大公共前缀后缀,方便后面的访问
for (int i = 1; i < len; i++){
while (k > -1 && str[k + 1] != str[i]) k = next[k];
if (str[k + 1] == str[i]) k++;
next[i] = k;
}
}
(二)KMP算法:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 206;
int next[maxn];
void ca_next(string str, int len){
int k = -1;next[0] = -1;//-1表示不存在最大公共前缀后缀,方便后面的访问
for (int i = 1; i < len; i++){
while (k > -1 && str[k + 1] != str[i]) k = next[k];
if (str[k + 1] == str[i]) k++;
next[i] = k;
}
}
int KMP(string ts, string ps){
int ts_len = ts.size(),ps_len = ps.size();
ca_next(ps, ps_len);
int k = -1;
for (int i = 0; i < ts_len; i++){
while (k >-1 && ps[k + 1] != ts[i]) k = next[k];
if (ps[k + 1] == ts[i]) k++;
if (k == ps_len - 1) return i - ps_len + 1;
}
return -1;
}
int main(){
string s = "ABCDABABGFABAC";
string p = "ABAB";
cout<<KMP(s,p)<<endl;
return 0;
}
五:难点解析:k = next [ k ]
1.相同的最长前缀和最长后缀的长度 就是k。
2.假设循环进行到 第i次,即已经计算了next[i],我们是怎么计算next[i+1]呢?
比如我们已经知道ababab,i=4时,next[4]=2(k=2,表示该字符串的前5个字母组成的子串ababa存在相同的最长前缀和最长后缀的长度是3,所以k=2,next[4]=2.那么对于字符串ababab,我们计算next[5]的时候,此时i=5, k=2(上一步循环结束后的结果)。那么我们需要比较的是str[k+1]和str[i]是否相等,其实就是str[1]和str[5]是否相等
(1)如果相等,那么跳出while(),进入if(),k++,接着next[i]=k。即对于ababab,我们会得出next[5]=3
(2)如果不等,我们可以用”ababac“描述这种情况。 不等,进入while()里面,进行k=next[k],这句话是说,在str[k + 1] != str[i]的情况下,我们往前找一个k,使str[k + 1]==str[i],程序的意思是说,一旦str[k + 1] != str[i],即在后缀里面找不到时,我是可以直接跳过中间一段,跑到前缀里面找,next[k]就是相同的最长前缀和最长后缀的长度。所以,k=next[k]就变成,k=next[2],即k=0。此时再比较str[0+1]和str[5]是否相等,不等,则k=next[0]=-1。跳出循环。
3. k--不可行:
k = next[k] 这一句其实包含了条件,str[k] = str[i - 1]且长度为k的前缀应该和str[i - 1]结尾的后缀相同,k--则有可能提前跳出循环,导致长度为k的前缀应该和str[i - 1]结尾的后缀相同不满足。比如串“acceaccc”,但q为最后一个c时,next[7]应该等于-1。但如果用k--来做的话,循环到i等于7时,next[6] = 2,k = 2,满足while循环条件,进入循环,当k=1时,跳出循环,str[k + 1] = str[7],得到next[7] = 2,正确的应该next[7] = -1。