一个有限自动机M是一个5元组 (Q,q0,A,Σ,δ) ,其中:
1:Q 是状态的有限集合;
2: q0 (属于Q)是初始状态;
3:A(属于Q)是特殊的接收状态的集合;
4: Σ 是有限输入字母表;
5: δ 是一个从 Q×Σ 到Q的函数,称为M的转移函数;
有限自动机开始于状态 q0 ,每次读入输入字符串的一个字符。如果有限自动机在状态q时读入了字符a,则它从状态q变为状态 δ(q,a) (进行了一次转移)。每当其当前状态q属于A时,就说自动机M接受了迄今为止所读入的字符串,没有被接受的输入称为被拒绝的输入。
为了构造字符串匹配自动机,我们还要定义一个辅助函数 σ ,称为对应P的后缀函数。假设有一个字符串X和y,这个y既是模式P的前缀同时又是X的后缀。 σ(X) 表示y所包含的字符个数的最大值,比如模式P为ababaca,X为ccab,那么y为ab,那么 σ(X)=2 ;
假设给定模式P,其字符个数为m,其相应的字符串匹配自动机定义如下:
1:状态集合Q为{0,1,2,…,m}。开始状态 q0 是0状态,并且只有状态m是唯一被接受的状态。
2: 对任意状态q和字符a,转移函数 δ 定义为 δ(q,a)=σ(Pqa) ; Pq 是由模式P的前q个字符组成的字符串。
我们假设当字符串匹配自动机从文本读入T[i]字符时,自动机一共所读入的字符串为 Ti ,假设字符串y既是 Ti 的后缀同时又是模式P的前缀,那么 σ(Ti) 表示字符串y所包含字符个数的最大值。
转移函数之所以定义成这个形式,是为了使字符串匹配自动机从文本中每读入一个字符T[i]后其自动机状态值为 σ(Ti) ,其证明可以参考课本定理32.4。那么很显然自动机状态为m表明模式P是字符串 Ti 的后缀,也就表明模式P在文本T中找到了,所以状态m是唯一被接受的状态。
该算法代码由两部分组成,一部分是计算转移函数 δ ,其相当于对模式的预处理,另一部分是利用计算好的转移函数将模式P与文本T匹配。代码如下:
//判断"P[0...k-1]"是否是"P[0..q-1]+c"这个字符串的后缀;
bool is_subset(const string& P,size_t k,size_t q,char c)
{
string new_string(P,0,q);
new_string=new_string+c;
for(int i=0;i!=k;++i)
if(P[i]!=new_string[q+1-k+i])
return false;
return true;
}
//计算转移函数delta(q,a);
vector<map<char,size_t>> compute_transition_function(const string& P,const string& charac_set)
{
size_t m=P.size();
vector<map<char,size_t>> delta(m+1);
for(size_t q=0;q<=m;q++) //q代表着状态
for(size_t i=0;i!=charac_set.size();++i)
{
size_t k=std::min(m,q+1);
while(!is_subset(P,k,q,charac_set[i]))
k--; //确定k以使得字符串"P[0...k-1]"是字符串"P[0..q-1]+charac_set[i]"的后缀;
delta[q][charac_set[i]]=k;
}
return delta;
}
//将模式P与文本T匹配,输入模式P在文本T中出现的位置。
void finite_automation_matcher(const string& P,const string& T,const string& charac_set)
{
size_t q=0;
auto delta=compute_transition_function(P,charac_set);
size_t m=P.size();
for(size_t i=0;i!=T.size();++i)
{
q=delta[q][T[i]];
if(q==m)
cout<<"the pattern starts to occur from the "<2-m<<"th character of Text"<
在该算法中,匹配时间为 Θ(n) ,但是上述代码中计算转移函数的预处理时间为 O(m3|Σ|) ,采用下面的代码能够把预处理时间缩短为 O(m|Σ|) 。
vector<map<char,size_t>> compute_transition_function(const string& P,const string& charac_set)
{
size_t m=P.size();
//计算前缀函数,KMP算法中会提到
auto pi=compute_prefix_function(P);
vector<map<char,size_t>> delta(m+1);
for(size_t q=0;q<=m;++q) //q表示状态
{
for(size_t i=0;i!=charac_set.size();++i)
{
size_t k=0;
if(q==0){
if(P[0]==charac_set[i])
k=1;
}
else if(q==m||P[q]!=charac_set[i]){
k=delta[pi[q-1]][charac_set[i]];
}
else
k=q+1;
delta[q][charac_set[i]]=k;
}
}
return delta;
}
有限自动机算法和 Knuth-Morris-Pratt 算法其实思路是一样的,都是为了使得从文本中每读入一个字符T[i]后状态值为 σ(Ti) ,当状态值为m时表明模式已经在文本中被找到了,但是实现的方法有点不一样。在有限自动机算法中,我们定义了一个状态转移函数 δ(q,a)=σ(Pqa) ,通过这个状态转移函数我们知道读入文本T[i]后的状态值,但是KMP算法用到了一个更加巧妙的方法:
当我们读入文本T[i-1]时,我们获得了一个状态值m’,这时字符串 Ti−1 的后缀为模式P的子字符串P[0… (m’-1)]。如果我们知道这个子字符串P[0…(m’-1)]的后缀与模式P的前缀最多有多少个字符匹配,假设有k个,当然k应该要小于m’,那么我们可以通过下面代码中KMP_matcher()函数for循环中的while和if语句来得到读入T[i]后的状态值。
在这里我们需要定义一个模式的前缀函数 π ,这个函数就是用来存储模式子字符串P[0…(m’-1)](m’>0&&m’
// 计算模式P的前缀函数$\pi$
vector compute_prefix_function(const string& P)
{
size_t m=P.size();
vector pi(m);
pi[0]=0;
size_t k=0; // number of characters matched
for(size_t i=1;i!=m;++i) // scan the pattern from left to right;
{ // 此时模式P的字串P[0...(i-1)]的后缀与模式P的前缀重合的部分有k个字符
while(k>0&&P[k]!=P[i])
k=pi[k-1];
if(P[k]==P[i])
k+=1;
pi[i]=k; //此时模式P的字串P[0...i]的后缀与模式P的前缀重合的部分有k个字符
}
return pi;
}
void KMP_matcher(const string& P,const string& T)
{
size_t n=T.size();
size_t m=P.size();
auto pi=compute_prefix_function(P);
size_t q=0; //number of characters matcher;
for(size_t i=0;i!=n;++i) // scan the text from left to right;
{// 此时模式T的子串T[0...(i-1)]的后缀与模式P的前缀重合的部分有q个字符
while(q>0&&P[q]!=T[i])
q=pi[q-1];
if(P[q]==T[i])
q=q+1;
//此时模式T的子串T[0...i]的后缀与模式P的前缀重合的部分有q个字符
if(q==m){
cout<<"The pattern starts to occur from the "<2-m<<"th character of text"<1];
}
}
}
KMP算法的预处理时间是 Θ(m) ,匹配时间为 Θ(n)