Brief Introduction
KMPKMP(Knuth-Morris-Pratt) 算法是一个著名的 字符串匹配 算法,他不仅简洁高效,更以 难以理解 而著名,为了更好的理解此算法,在博客里简单的记录一下我的理解,如有错误欢迎指正,也希望能给大家带来一点启发。
Principle
为了更方便讲解,我们把被匹配的字符串称为文本串,要找出的字符串称为模式串。我们首先思考最简单的字符串匹配—— “朴素字符串匹配”,朴素字符串匹配就是把模式串不停的去与文本串作对比,如果遇到不匹配的,就把模式串第一位后移一位,指针再从后移后的第一位开始比较,不难看出,朴素字符串匹配的思路简单明了,但是时间复杂度高的要命——O(n*m)
于是我们一定有更加高效简洁的算法来解决此问题(当然啦不然KMP是用来干啥的)。思考:我们每次可以跳多位,而不只一位,从而大幅提升效率。
我们先提出解决方法:每次遇到不匹配的,模式串可以直接跳到失配位之前子串的最大公共前后缀的前缀的后面。(绕口)
其实很简单,我们举个例子
文本串 ABABCAB
模式串 ABABA
我们从第一位开始匹配
ABCABCAB
ABCABA~ (状态一)
发现到第五位失配 按照朴素字符串匹配算法此时应该是模式串向后一位再匹配
ABCABCAB
~ABCABA~ (状态二)
但既然我们要学习KMP算法就肯定不可能和朴素匹配一样吧(废话) 我们仔细观察这个例子可以发现我们可以直接跳到下面这一个状态(这个状态就直接匹配好了)
ABCABCABA
~~~ABCABA (状态三)
我们观察他是如何跳转的 在状态一下 我们发现是在第六位失配 在kmp算法下 直接跳到状态三 模式串向后移了三位 那我们是如何确定直接让模式串后移三位呢? 我们观察模式串 ABCABA
仔细观察后 我们引入一个概念 最大公共前后缀 在认真观察和了解最大公共前后缀的概念有想法了吗? (没有的话很正常)我们先直接了解KMP算法的思路 在某一位失配时 直接跳到这一位之前的子串的最大公共前后缀的前缀的后一位继续匹配
我们结合例子看
例如 ABCABA在第六位失配 此时需要考虑的子串是前五位ABCAB 最大公共前后缀和AB 所以我们跳到前缀的后一位“C”继续与文本串的的六位匹配。
(如果还不理解多结合几个例子看)
但是CC语言中并没有使数组前后移动再匹配的实现方式 我们思考如何运用CC语言的思考方式来 实现KMPKMP
首先我们用两个指针I,J来分别指向文本串和模式串,I每次都是一位一位的跳转,而遇到失配位置时,J就要跳到其子串的最大公共前后缀的前缀的后一位 既然每次遇到失配位置时 子串是固定的 指针跳转的方式也是确定的 无论文本串是什么样子的 失配位置的可能性是有限的——模式串的长度 那么我们是不是可以先预处理出最大公共前后缀的长度 然后每次遇到失配位置时跳转就可以了。那么预处理出来的数组就是kmp算法的精髓——next数组
Code
Tips1 我在学习过程中一直有一个误解 以为 next[i]是第I位失配时J需要跳转的位置,但其实next[i]是前II位子串的最长公共前后缀长度。那么也可显而易见 next[1]=next[2]=0
我们先看next数组求法 思考 next数组的可继承性
我们依然是举例说明
设有模式串 假设我们现在在求 next[16](前十六位的公共前后缀的长度)而我们设此时next[15]=7那是不是意味着第1-7位(前缀) 和第9-15位(后缀)是一模一样的
那么此时有两种情况:
第八位和第十六位是一样的 此时显而易见next[16]=next[15]+1
那如果不一样怎么办呢? 那我们可以看next[7],这是为什么呢?我们假设next[7]=3 那是不是意味着第1-31位和第5-7位是一样的? 那又因为第1-7位和第9-15位是一模一样的 所以第1-3位、第5-7位、第9-11位和第13-15位都是一样的 那我们此时可知第1-3位和第9-15位是一样的 我们只需要看第4位和第16位是不是一样的 如果是一样的 此时 next[16]=next[7]+1
这就是next的可继承性。文字说明确实难以理解 还不懂的朋友的可以手画一下。
看代码
j=0;
for (int i=2;i<=lb;i++){
while(j==1&&b[i]!=b[j+1])//如果是j=0的话就不用跳了
j=next[j]; //不停的回溯
if(b[j+1]==b[i])j++;
next[i]=j;
//如果一致了或者没有一样就求出了next[i]
}
当我们求出了next数组后 后面就可以轻松搞定了 最后提醒一下在文本串和模式串真正匹配的时候 J的意义是模式串匹配了几位 当出现不匹配的时候就直接向前跳转而如果j=lenb时 就代表都成功匹配,而此时就代表模式串都成功匹配。
#include<bits/stdc++.h>
#define G 1000010
using namespace std;
int next[G];
int lena,lenb,j;
char a[G],b[G];
void deal_next(){
for(int i=2;i<=lenb;i++){
while(j&&b[i]!=b[j+1]) j=next[j];
if(b[j+1]==b[i])j++;
next[i]=j;
}
}
void deals(){
j=0;
for(int i=1;i<=lena;i++){
while(j>0&&b[j+1]!=a[i]) j=next[j];
if(b[j+1]==a[i]) j++;
if(j==lenb){
cout<<i-lenb+1<<endl;
j=next[j];
}
}
}
int main(){
cin>>a+1;
cin>>b+1;
lena=strlen(a+1);
lenb=strlen(b+1);
deal_next();
deals();
for (int i=1;i<=lenb;i++)
cout<<next[i]<<" ";
return 0;
}
In the End
本文疏漏可能偏多,初衷也是巩固自己的学习成果,借鉴了诸位大佬的博客/视频/代码,如有差错欢迎私信评论指正,希望大家能学好Kmp!