第32章 :字符串匹配—有限自动机算法,Knuth-Morris-Pratt算法

有限自动机算法:

一个有限自动机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算法:

有限自动机算法和 Knuth-Morris-Pratt 算法其实思路是一样的,都是为了使得从文本中每读入一个字符T[i]后状态值为 σ(Ti) ,当状态值为m时表明模式已经在文本中被找到了,但是实现的方法有点不一样。在有限自动机算法中,我们定义了一个状态转移函数 δ(q,a)=σ(Pqa) ,通过这个状态转移函数我们知道读入文本T[i]后的状态值,但是KMP算法用到了一个更加巧妙的方法:

当我们读入文本T[i-1]时,我们获得了一个状态值m’,这时字符串 Ti1 的后缀为模式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)

你可能感兴趣的:(算法导论-CLRS)