KMP字符串匹配算法(
Knuth–Morris–Pratt string searching algorithm, KMP algorithm
) 是由
Donald Knuth
,
Vaughan Pratt
和
James H. Morris
三人设计的线性时间字符串匹配算法。
基本思想
传统的朴素匹配算法在失配之后,仅仅将模式字符串右移一个单位继续匹配。但在这些右移的过程中,多数右移是不匹配的,因此我们希望尽快的找出下一个可能的位移。例如
abcabaabcabaabcabac ||||||||||||* abcabaabcabac
朴素匹配是仅仅右移一位
abcabaabcabaabcabac * abcabaabcabac
而理想的情况是右移6位,可以大大提高匹配的效率。
abcabaabcabaabcabac ||||||||||||| abcabaabcabac
现在的问题是如何在失配时获得理想的偏移量。通过思考可以发现,从模式匹配的开始位置到失配位置,待匹配串的内容都是和模式字符串的内容相同的,因此理想的偏移量的大小实际上就是模式字符串需要右移几个单位跟已经匹配的部分的后缀匹配。由此可知,每当失配是理想偏移量的大小仅仅与模式字符串本身有关,而与待匹配字符串无关。
abcabaabcabaabcabac ||||||||||||* abcabaabcabac |||||| abcabaabcabac
这样的理想偏移量被成为模式字符串的前缀函数,在本文的前缀函数一节中给出定义已经计算方法。根据前缀函数,我们可以在字符串失配时快速的右移模式字符串来完成字符串的查找匹配,这就是KMP算法,具体实现在本文KMP算法一节中给出。
前缀函数
模式字符串的前缀函数π用于表示模式字符串与其自身的偏移进行匹配的信息,可以用于在朴素字符串匹配算法中避免对无用偏移的监测。
π[q]是模式字符串中长为q的前缀字符串的后缀中最长前缀的长度,即π[q]=max{k|k<p且Pk ⊐Pq}.接下来的问题在于如何求出π[q].
我们可以归纳的求出π[q].
- 首先我们知道π[1]=0
- 其次,假设π[1..k]已知,我们考虑如何求出π[k+1]。
- 若P[k+1]=P[q]则显然π[k+1]=q+1
- 如果P[k+1]≠P[q]则根据已经获得的后缀函数q=π[k],π[π[k]],π[π[π[k]]]…不断尝试尝试P[k+1]是否和P[q+1]匹配,如果匹配则π[k+1]=q+1,否则需要继续右移尝试,直到q为0.
下面给出前缀函数计算的C++代码。
//P is start from index 1 //for example in C++, P=#abceabaabceabc vector<int> computePrefixFunction(string &P){ int len=P.size(); vector<int> pi(len); pi[1]=0; for(int k=0,q=2;q<len;++q){ while(k>0&&P[k+1]!=P[q])k=pi[k]; k+=(P[k+1]==P[q]); pi[q]=k; } return pi; }
KMP算法
根据上一节的算法求出模式字符串的前缀函数之后,根据基本思想很容易写出线性时间内字符串匹配算法,给出C++代码如下。
//S and P are both start from index 1 //for example in C++, P=#abceabaabceabc int KMP_Macher(string &S,string &P){ int ls=S.size(),lp=P.size(); vector pi=computePrefixFunction(P); for(int q=0,i=1;i<ls;++i){ while(q>0&&P[q+1]!=S[i])q=pi[q]; if(P[q+1]==S[i]){ ++q; if(q==lp-1)//return the index of first match return i-lp+1; } else q=pi[q]; } return -1;//No matching! }