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=3next[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,bint 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;elseretun -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

浙公网安备 33010602011771号