KMP算法的详细阐述
KMP详细解释
basic
algorithm
前言
自己在做leetcode的时候,遇到了一道字符串匹配的问题,开始的时候用的是c++中的自带库以及定制操作find,而后在讨论区看到了KMP算法,遂去了解,但是没想到这个算法对我来说,如此晦涩难懂,今天终于算是有了个一知半解,所以将其写下来。
KMP的出现
在我们平常做这种字符匹配的题目时,大部分人第一想法都是使用暴力破解
对于s主串 ababababca 有一个 匹配串abababca 需要去匹配自身在其中的位置
我们大部分的想法,就是一个i指针指向s主串,一个j指针指向匹配串。
然后i所指的与匹配串j所指的一一进行匹配,如果遇到不相同的就将i+1,j归零,再次匹配
即
if(s[i]==p[j]){
i++;
j++;
}
else{
i=i-j+1;
j=0;
}
- 但是发现了吗,我们在重复的匹配之前已经匹配过的字段,重复的去做一些已经做过的工作,
如 S串:ababababca与P:abababca的暴力破解中,第一次执行到i=6,j=6的时候发生了不匹配,这时候就将再将i调整到第二位,然后j归零,再次进行比较,但是我们之前所匹配成功的字段ababab
就这样浪费了,没有用到他,而kpm的核心就是用到了这个已匹配字段的信息。
所以,kmp是通过利用已经匹配字段的信息,来加快字符匹配,以及减少重复工作。时间复杂度o(n+m);
KMP的核心思想
我在看博客的时候,一直不明白的其实就是next数组的求解,其实next数组就是一个前缀后缀的最大集合的一个简化版,因为next确实不等于前缀后缀的最大集合,但其实就是一个对于最大前缀后缀的后移。
这篇文章讲的十分清楚https://www.zhihu.com/question/21923021/answer/281346746
意思就是,对于abababca中
a | b | a | b | a | b | c | a |
---|
其中
对于a来说,无前后缀,则value值为0
对于ab,无前后缀相等,则value为0
对于aba,有相同前后缀,{a},长度为1,则value为1
对于abab ,有相同前后缀,最长{ab},长度为2,则value为2
对于ababa,有相同前后缀,最长为{aba},长度为3,则value为3
对于ababab,有相同前后缀,最长为{abab},长度为4,则value为4
对于abababc,无相同前后缀,value为0
对于abababca
s index | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value | 0 | 0 | 1 | 2 | 3 | 4 | 0 | 0 |
而以上的只是相同前后缀的值,称作PMT,而我们将PMT放在匹配字段中看一下
就像我们开始那样,对于
主串 | a | b | a | b | a | b | a | b | c | a |
---|
匹配串 | a | b | a | b | a | b | c | a |
---|
当i=6,j=6时,两个字符不匹配,一个为a一个为c,因为在之前我们就发现,匹配字段中(0~j-1)与主串中(i-j,i-1)的位置是匹配的,所以我们去找寻这个一个字段相同前后缀的值,这个字段是
|a|b|a|b|a|b
|:|
而我们找到这个字段 他的value值就是4,意思就是说,他的前后缀有4个位置是相匹配的,所以我们就可以在此基础上将匹配串向右移动两个位置(移动位置=此时j指针所指的位置-value值)PS:匹配串移动两个位置其实就是j指针向左移动两个位置,然后izhi'zh
,则我们就可以跳过abab的匹配,因为它的前后缀是相同,肯定是已经配上了,所以节省了很大的工作。
就是使用相同前后缀的值所带来的效果。
那么既然每次不匹配的时候,我们都要从当前不匹配字符向前移动一个字符,去寻找他的相同前后缀值,那么为何不节省一点,直接将整个value数组向后移动一个位置
s index | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value-next | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
其实这就是value数组的来历,就是将PMT向后移,更好的适配我们的程序
那么这个时候我们的代码就可以读懂了
void MakeNext(string p ,vector<int> next){
next.reserve(p.length());
next[0]=-1;//产生后缀
int overfront=-1;//指向匹配字段的头部以前,其实就是为了构建出一个后缀。
int i=0;
while(overfront<p.length()-1){//开始匹配字段的自我比较,找寻相同前缀后缀码
if(overfront==-1||p[overfront]==p[i]){
++overfront;
++i;
next[i]=overfront;
}
else{
overfront=next[overfront];//overfront回溯,以求在i之前找到匹配字段,如果没有最后会回溯到overfront为-1,然后再进入上一步,将其value值设定为0
}
}
}
以下是进行一个伪代码运行
第一次 overfront(后面简称of)=-1,i=0;
++of;
++i;
next[i]=of;
此时next数组
next-index | 0 | 1 |
---|---|---|
next-value | -1 | 0 |
此时i=1,of=0;
进行判断of=0,且p[of]!=p[i];
of=next[of];
of=-1;
of=-1;i=1;
++overfront;
++i;
next[i]=overfront;
此时的next数组
next-index | 0 | 1 | 2 |
---|---|---|---|
next-value | -1 | 0 | 0 |
of=0,i=2;
because p[of]==p[i]
++of;
++i;
of=1,i=3
next[i]=of
此时的next数组
next-index | 0 | 1 | 2 | 3 |
---|---|---|---|---|
next-value | -1 | 0 | 0 | 1 |
如此依次往下
最后得出
s index | a | b | a | b | a | b | c | a |
---|---|---|---|---|---|---|---|---|
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
value-next | -1 | 0 | 0 | 1 | 2 | 3 | 4 | 0 |
那么在完成了next数组求值之后,我们就要开始进行字符串的匹配,就跟我们开始讲的那样,一步步的匹配,然后遇到不匹配的回溯。
代码如下
int malepular(string a,string b){
int i=0;
int j=0;//两个指针分别指向a,b
int pren=a.length();
int tpre=a.length();
while(i<pren&&j<tpre){
if(j==-1||a[i]==b[j]){++i;++j;}
else{
j=next[j];//
}
}
if(j==tpre)return i-j;
else
retun -1;
}
参考文献
http://wiki.jikexueyuan.com/project/kmp-algorithm/define.html
https://www.zhihu.com/question/21923021/answer/281346746
https://www.bilibili.com/video/av3246487?from=search&seid=15447356355389591502