KMP字符串匹配算法(二)—KMP要点和证明
在朴素字符串的匹配算法中,查找模式串P在字符串S中的匹配是一种walk-one-by-one的过程,即从S[i]开始匹配,一旦在S[j] (j−i+1<P.length) ( j − i + 1 < P . l e n g t h ) 处无法匹配,算法只能前进一步,还得从S[i+1]从头开始匹配……。
KMP字符串匹配算法就比朴素字符串的匹配算法高明,它会事先处理模式串P,充分利用P的信息,做到“jump”式的匹配,就像有限自动机一样,在当前状态,遇到某个输入,跳转到新的状态。
例如: 模式串P=abcabcabfg, 文本串S=abcabcabcabcabfg。
从S[0]开始匹配,可以看到,P[8] = ‘f’ != S[8]=’c’。按照朴素算法,下一次匹配得从S[1]开始。可是我们明明都匹配到了C=abcabcab,重新来过?不甘心!能不能利用C的信息呢?[注:因为C是P的一个前缀,C的所有信息只是P的信息的一个子集,说到底,其实也就是利用模式串P的预处理信息。]
朴素算法walk-one-by-one的方式会把所有的可能解都验证一遍,也就是在做穷举。
观察C,找出一个C的前缀F,使得1).F和C的一个后缀相同;2).满足1)且字符串长度最长。显然F是唯一的:F=abcab。
由于F满足1),所以省去了从头比较的必要,直接从S[8]和P[5]开始比较(前5个字符已对齐,无须比较),节省了比较次数,由于满足2)所以不会跳过任何可能匹配的case,因为如果只是满足1),那F’=ab也满足,直接从S[8]和P[2]开始比较(前2个已对齐,无须比较),但是这样P[0]一下跳了C.length-F’.length= 8-2=6步,从与S[0]对齐一下子跳到了与S[6]对齐,这下直接就跳过了F这种情况[注:对于前缀F,P[0]只跳了C.length-F.length=8-5=3步, 从与S[0]对齐跳到与S[3]对齐],肯定是不对的。
字符串的前缀函数 next[q] n e x t [ q ]
从上面简单的描述中我们发现求出模式串P每一个位置q的前缀 Fq F q 很重要,既然 Fq F q 是P的前缀,那么我们其实只要知道 Fq F q 的长度就行了。这个长度我们管他叫 next[q] n e x t [ q ] :
定义函数 next(q)=max{k∣k<q∧Pk⊃Pq} n e x t ( q ) = m a x { k ∣ k < q ∧ P k ⊃ P q } 为字符串 P P 的前缀函数
通俗的讲 next[q] n e x t [ q ] 就是能作为 Pq P q 的真后缀的 Pq P q 的最长前缀 Pk P k 的长度 k k 。
P的前缀函数迭代集 next∗[q] n e x t ∗ [ q ]
next n e x t 迭代得到的集合记为 next∗[q] n e x t ∗ [ q ] :
next∗[q]={next[q],next1[q]...nexti[q]...0} n e x t ∗ [ q ] = { n e x t [ q ] , n e x t 1 [ q ] . . . n e x t i [ q ] . . .0 }
(1)
其中 nexti[q]=next[nexti−1[q]] n e x t i [ q ] = n e x t [ n e x t i − 1 [ q ] ] ,括号中的迭代过程一直进行,直到某一步 nexti[q]=0 n e x t i [ q ] = 0 时停止迭代。
1. 用 P P 的前缀函数对 q q 迭代就能求出 Pq P q 的所有满足 Pk⊃Pq∧k<q P k ⊃ P q ∧ k < q 的前缀 Pk P k 的长度 k k 。(显然 Pk P k 既是 Pq P q 的真前缀又是 Pq P q 的真后缀)
证明: 参见算法导论第三版
集合 Eq−1 E q − 1
Eq−1 E q − 1 中的元素为 {k∣P[k+1]=P[q]∧k∈next∗(q−1)} { k ∣ P [ k + 1 ] = P [ q ] ∧ k ∈ n e x t ∗ ( q − 1 ) } 。
怎么理解这个定义呢?这样理解吧:我们把集合 Eq−1 E q − 1 看做是从集合 next∗(q−1) n e x t ∗ ( q − 1 ) 中筛选一部分值,什么样的值呢?从定义我们知道 next∗(q−1) n e x t ∗ ( q − 1 ) 其实就是满足既能作为串 Pq−1 P q − 1 的前缀又能作为它的后缀的所有前缀的长度的集合(长度和前缀是一一对应的关系),再看看集合 Eq−1 E q − 1 定义中要求的条件 P[k+1]=P[q] P [ k + 1 ] = P [ q ] ,你想到了什么?
集合 Eq−1 E q − 1 中的每一个值加一对应的都是 Pq P q 的一个既能作为 Pq P q 前缀又能作为 Pq P q 后缀的前缀的长度。
故集合 Eq−1 E q − 1 中的元素 k k 加1之后对应的前缀 Pk+1 P k + 1 肯定是 Pq P q 的一个后缀,所以有 Pk+1⊃Pq⇒k+1∈next∗[q] P k + 1 ⊃ P q ⇒ k + 1 ∈ n e x t ∗ [ q ] 。 可以说 集合 Eq−1 E q − 1 和集合 next∗[q] n e x t ∗ [ q ] 是一一映射的。( Eq−1 E q − 1 中的每一个元素 i i 加1之后的值 i+1 i + 1 都会落到集合 next∗[q] n e x t ∗ [ q ] 中, next∗[q] n e x t ∗ [ q ] 的每一个元素 i i 减1之后的值 i−1 i − 1 都会落到 Eq−1 E q − 1 中)。
所以你知道为什么要求集合 Eq−1 E q − 1 了吗?因为我们想只通过简单的比较 字符P[k+1]是否等于字符P[q] 字 符 P [ k + 1 ] 是 否 等 于 字 符 P [ q ] ,从集合 next∗(q−1) n e x t ∗ ( q − 1 ) 推导出集合 next∗[q] n e x t ∗ [ q ] 。这样就能迭代的求出 next(q) n e x t ( q ) 。
求 next[q] n e x t [ q ] 的理论依据
还记得吗,我们想要求出的是 next(q) n e x t ( q ) 。好了,经过上面的铺陈,现在终于可以心安理得的求 next[q] n e x t [ q ] 了:
next[q]={01+max{k∈Eq−1}Eq−1=∅Eq−1≠∅ n e x t [ q ] = { 0 E q − 1 = ∅ 1 + m a x { k ∈ E q − 1 } E q − 1 ≠ ∅
(2)根据这个结论,在求
next[q] n e x t [ q ] 时,迭代地在
next∗[q−1] n e x t ∗ [ q − 1 ] 中按递减的顺序去寻找一个
k k 满足
k∈Eq−1 k ∈ E q − 1 ,这样,如果这样的
k k 存在,那么第一个被找到的
k k 就一定是
Eq−1 E q − 1 集合中最大的
k k ,根据等式(2),
next[q]=k+1 n e x t [ q ] = k + 1 ,如果不存在,好办了,根据等式(2)直接得到
next[q]=0 n e x t [ q ] = 0 。两点说明:
– 对
next∗[q−1] n e x t ∗ [ q − 1 ] 的迭代顺序按照等式(1)中从左到右的顺序:
从i=0,1,... 从 i = 0 , 1 , . . . ,只有这样才能保持迭代得到的待定项
k k 是递减的。–
0∈next∗[q−1] 0 ∈ n e x t ∗ [ q − 1 ] ,所以迭代是良定义的,不用单独把0拿出来考虑。
代码
#include<iostream>
#include<string>
using namespace std;
void ComputeNext(string P,int *next,int const &n){//填上模式串P的next数组
next[0] = 0;//对应于next[1]=0; 第一个字符的前缀函数值为0.
int k(0);//k = next[0]=0;
for (int i(1); i < n; i++){
//【这里的i对应于第i+1个字符】---所以每一轮循环是在求【next[i+1]】
while (k>0 && P[i] != P[k]){//初始的 k = next[i-1],由上一轮循环得到【对应于k=next[i]】
//【这里P[i] != P[k]对应于P[i+1]!=P[k+1]】
k = next[k];// 【对应于迭代k = next[next[i]]】
}
if (P[i] == P[k])
k = k + 1;
next[i] = k;//同时也是下一轮的k的初始值 k=next[i]
}
}
/*对于模式串来说,我们会提前计算出每个匹配失败的位置应该移动的距离,花费的时间是常数时间*/
/*在已经匹配的模式串子串中,找出最长的相同的前缀和后缀,然后移动使它们重叠*/
int KMP_v1(string T,string P){
int shift(0);//T的与P匹配的第一个子串P'的第一个字符距离起始位置的偏移量。
int lengthP = P.size();
int lengthT = T.size();
int *next = new int[lengthP];
ComputeNext(P,next, lengthP);//计算前缀函数next
for (int t(0) ,p(0),index(0); index <lengthT; index=t ){
if (P[p] == T[t]){
p++; t++;
if (p == lengthP){ break; }//找到匹配,退出循环。
}
else{
if (p>0){//P和T已经匹配了一部分字符。
p = next[p - 1];//进行下一次比较的P的字符的位置。
}
else t++;//P和T没有匹配上任何字符,也就是P和T这一轮第一次字符匹配就失败,这时就要向后移动t
shift = t - p;//T的下一次比较中与P的首字符对齐的字符距离起始位置的位移量
}
}
return shift;
}
int main(){
string T;
string P;
cout << "输入文本T:" << endl;
cin >> T;
cout << "输入模式串P:" << endl;
cin >> P;
int s = KMP_v1(T,P);
if (s < T.length()){
cout <<P<<" 在 "<<T<<" 中偏移量 "<<s <<" 处"<< endl;
}
cout << endl;
system("pause");
}
至于KMP算法该怎么利用前缀函数 next n e x t ,也就是说求得了 next n e x t 之后要怎么做,怎么利用 next n e x t 来加快匹配速度呢,我相信不做解释,看看代码,在草纸上画画就能明白了。在考虑 next n e x t 的作用时仔细想想它是怎么定义的。