算法导论-字符串匹配

编辑文本时,我们经常需要在文本中找到某串模式字符在整个文本中出现的位置,这个模式字符串即为用户查找输入的关键字,解决这个问题的算法为字符串匹配算法。
算法导论-字符串匹配_第1张图片
当我们遇到这个问题,如何查找在文本中出现的模式呢?
一:朴素字符串匹配
我们假设存在两个游标i,j分别指向文本串与模式串的位置,那么有
1:当匹配到T[i]==P[j],则i++,j++;
2:当在匹配到某一位置时出现T[i]!=P[j]时,即匹配失败,此时i回溯到本次开始匹配的位置,j=0
咱们从代码与匹配图解理解朴素算法思路:

int native_string_matcher(char* T,char* P)
{
//获取原始串和模式串字符长度
    int n=strlen(T);
    int m=strlen(P);
    int s=0,i=0;
    //原串开始从0至n-m偏移,以匹配模式串
    for(s=0;s<=n-m;s++)
    {
//模式串从0-m开始分别匹配模式串中字符是否与原串相等
        for(i=0;iif(P[i]!=T[s+i]) //如果在匹配过程中有字符不相等,则跳出该循环,偏移S向下移位,继续重新匹配
            {
                    break;
            }
            if(i==m-1)//当i=m-1,且P最后字符与T最后字符相等,则表示字符串匹配成功,此时返回原串中与模式串相匹配的起始位置。
                    printf("has match");
                    return s;
        }
    }
}

OK,现在我们举个栗子,
假设有一个文本字符串和模式字符串如下图
算法导论-字符串匹配_第2张图片
(a) 字符串开始匹配,此时T[0]=P[0],i++为1,此时s+i=1,T[1]!=P[1],匹配失败,s自加1,变为1,开始图(b)的匹配过程;
(b) 此时T[s=1]=c!=P[0],匹配失败,s自加1,变为图c的匹配过程
(c) 此刻T[s=2]=P[0],i自加1,T[s+i=3]=P[i=1],字符相等,继续下一匹配,i自加1为2,T[s+i=4]=P[i=2],且此刻i=length(P),匹配结束。此刻,s继续自加,进行余下字符串的匹配。
朴素字符串匹配过程较简单,但是最坏情况下时间复杂度为O((n-m+1)*m),字符串的匹配时间开销较大,并不能很好的解决字符串的匹配。
补充:
在朴素字符串匹配中存在一种特殊情况,即模式串P中的所有字符都不相同,其匹配时间可以达到O(n),具体的实现代码如下:

int native_string_matcher(char* T,char* P)
{
//获取原始串和模式串字符长度

    int n=strlen(T);
int m=strlen(P);
    int j=0,i;
    for(i=0,j=0;iif(T[i]==P[j]&&jelse{
                if(j!=0)
                {
                    j=0;
                    --i;
                }
            }
            printf("%d\n",i);
            if(j==m)
                return i-m+1;
    }
    return -1;//若为-1,代表原始串中不包含模式串
}

这个实现过程很好理解,下面图示匹配过程:
算法导论-字符串匹配_第3张图片
整个匹配流程如下:
(1) 文本串i=0,模式串j=0,此时T[i]!=P[j],匹配失败,i自加1;
(2) 开始匹配,T[1-4]=P[0-3],但T[5]!=P[4],匹配失败,由于P中所有字符互不相同,所以i不必回溯到T[i=2],只需开始匹配T[i-1=4]与P[0]并开始新一轮的匹配过程。

至此,朴素字符串匹配算法结束,整个过程很好理解。

二Rabin-Karp算法

    在实际应用中,Rabin-Karp算法的预处理时间为O(m),并且在最坏的情况下的时间复杂度为O((n-m+1)m),相对于朴素字符串,它的运行时是比较好的。整个算法思想介绍如下:

以10进制数来表示字符,例如笔者用0表示a,t同理1表示b,以此类推,那么字符串dbebf可以表示为31415,将字符串看做数字,在算法匹配中更加方便。
给定一个模式串P[1-m],同理在文本串T[s+1..s+m]表示文本串中某段与模式串长度相等的一串字符,那么分别用数字p,t表示这两个字符串,通过比较数字p和t的大小,就可确定字符串是否匹配而无需用模式串中字符依次匹配文本串。示例图如下:

算法导论-字符串匹配_第4张图片
算法的思想理解之后,我们需要思考如何计算p和t.
数学中有霍纳法则,我们运用霍纳法则在O(m)内计算p:
p=P[m]+10(P[m-1]+10(P[m-2]+…+10(P[2]+10P1)…)))
霍纳法则的解释如下:

运用霍纳法则,类似的我们也可以根据T[s+1…s+m]计算出t.
但为了节约时间,我们可以利用一下方法在常数时间内根据ts,计算出ts+1.具体过程如下图解:
算法导论-字符串匹配_第5张图片
如图所示,ts=31415,ts+1=14152,则
ts+1=(ts-(T[s+1]=3)*10^(m=4))*10+(T[s+m+1]=2)
注:(ts-(T[s+1]=3)*10^(m=4))=31415-30000=1415
即 ts+1=10(ts-10^(m-1)T[s+1])+T[s+m+1]
在计算过程中,可能会出现p与t的值过大,可以取模运算。
下面的代码实现了上述的思想:

#include"stdio.h"
#include"string.h"
#include"math.h"

char*  substr(char* p,int start,int end)
{
        char *q=malloc(sizeof(char)*(end-start+1));
        int i;
        for(i=0;i1;i++)
             *(q+i)=*(p+start+i);
       // *(q+end-start+1)='\0';
        return q;
}//取T中T[s+1,s+m]子字符串
void RABIN_KARP_MATCHER(char T[],char P[],int d,int q)
{
    int n,m,h,p,t,i,t0=0;
    n=strlen(T);
    m=strlen(P);
   h=((int)pow(d,m-1));
  // h=((int)pow(d,m-1))%q;//取模运算
    p=0;
    t=0;
    for(i=0;i<m;i++)
    {
        p=(d*p+P[i]-'a')%q;//求p
        t =(d*t +T[i]-'a')%q;
        t0=(d*t0 +T[i]-'a');//求初始t0=num(T[1,m])
    }
    int s;
    for(s=0;s<=n-m;s++)
    {
            if(p==t&&(strcmp(P,substr(T,s,s+m-1))==0))
         //  if(p==t)
           {

                printf("%d\n",s);
           }

           printf("%d  %d  %c \n",t,p,T[s]);
           int tem=t0;
           t0=(d*(tem-(T[s]-'a')*h)+T[s+m]-'a');//求ts+1;
           t=t0%q;
  //              int tem=t;
   //             t=(d*(tem-(T[s]-'a')*h)+T[s+m]-'a')%q;

    }
}
int main()
{

        char T[100], P[100];
        scanf("%s",T);
        scanf("%s",P);
        RABIN_KARP_MATCHER(T,P,10,13);

        return 0;
}

三:有限自动机

有限自动机:每读入字符串的一个字符,则其状态从当前q0转变为q(a)。
对于给定的的模式P=ababaca,首先定义一个函数f()为P的后缀函数,满足f(x)是x的后缀是P的最长前缀,

举个栗子:
f(ccaca)=1 x=ccaca,是x的后缀同时是P的最长前缀的是字符a,所以
f(ccaca)=1
同理f(ccab)=2

定义转移函数g(q,a):q为任意状态和字符a,则定义如下
g(q,a)=f(Pqa);记录当前状态q与当前字符a时已得到的与模式P匹配的文本字符串T的最长前缀。
原理:
自动机处于状态q并且读入下一个字符T[i+1]=a,那么此时这个状态转换是Tia的后缀,同时是P的最长前缀,记为f(Tia)。

实例解释:
算法导论中例子:
算法导论-字符串匹配_第6张图片

T=abababacaba,P=ababaca
此时T中的字符为a,b,c这三种,那么对于状态从0开始,加入从T中读入a,则f(T1a)=1,T1a=a,T1a的后缀为P的最长前缀=a;假如读取字符为b/c,
则T1b/c的后缀为b/c,同时又是P的最长前缀的字符没有,状态为0
所以state 0=1,0,0
对于state=1,从T2中读取a,则T2a=aa,是T2a的后缀同时又是P的最长前缀=a,f(T2a)=1;假如读取字符为b,则T2a=ab,是T2a的后缀同时又是P的最长前缀=ab,所以f(T2a=b)=2,同理假如读取字符为c,则ac后缀与P的最长前缀为空,f(T2a=c)=0
依次推算,可以理解状态转移表中内容。

由上可知有两种情况:
第一种情况 a=P[q+1],使字符a可以继续匹配,那么可以沿着自动机主线(图中灰色部分)继续进行
第二种情况:a!=P[q+1],此时我们需要找到一个更小的子串,满足它是Tia的后缀同时是P的最长前缀。如f(5,b)=4,是因为状态q=5时读取字符b
此时T5a=ababab,P=ababaca,是T5a的后缀同时又是P的最长前缀=abab,所以f(5,b)=4.
对于Tia的后缀,同时是P的最长前缀下图可以理解
设Tia=ababcab P=abaca
这里写图片描述
即Tia从后往前与P的最大交集(这样解释虽然不科学)
理解了这个过程,则程序如下:

#include
#include 

#define min(x,y) (x
#define MAX_LEN 100
#define MAX_CHAR 4

int state[MAX_LEN][MAX_CHAR];
int Prefix_cmp(char *P,int k,int q, char a) //求后缀
{
    if(k==0)
        return 1;
    if(k==1)
    {
        return P[k-1]==a;
    }
    return P[k-1]==a&&(strncmp(P,P+q-k+1,k-1)==0);
}
void COMPUTE_TRANSITION_FUNCTION(char *P,char *A)//计算转移函数
{
    int m,q,k,i;
    m = strlen(P);
    k = 0;
    printf("%d  \n",m);
    for(q=0;q<=m;++q)
    {
        for(i=0;i1;++i)
        {
            k=min(m+1,q+2);
            do
            {
                k=k-1;
            }while(!Prefix_cmp(P,k,q,A[i]));//循环直至找出k,使Pk是(Pq+a)的最大后缀
            state[q][i]=k;
            printf("%d|%d  ",k,q);
            printf("%s\n",P);

        }
    }
    printf("\n");
    for(i=0;i<=m;++i)
    {
        for(q=0;q1;q++)
            printf("(%d)%d ",i,state[i][q]);

        printf("\n");
    }
}
void FINITE_AUTOMATON_MATCHER(char *T,char *P,char *A) //根据转移函数匹配字符串
{
    int n,m,i,q;
    n=strlen(T);
    m=strlen(P);
    q=0;
    COMPUTE_TRANSITION_FUNCTION(P,A);
    for(i=0;i'a'];
        if(q==m)
            printf("%d\n",i-m);
    }
}
int main() {
    char T[MAX_LEN],P[MAX_LEN],A[MAX_CHAR];
    scanf("%s",A);
    scanf("%s",P);
    scanf("%s",T);
    FINITE_AUTOMATON_MATCHER(T,P,A);
    return 0;
}

四:KMP算法

理解KMP算法,首先得理解PI数组的作用。
以朴素字符串的匹配过程为例:

这里写图片描述
如图所示:在模式P匹配文本T时,当匹配到最后一位不匹配时,朴素字符串匹配的做法是P往前移动一位继续匹配:
这里写图片描述

可是根据我们的观察发现更有效的做法是P可以直接往前移动两位,如下
这里写图片描述

如上个人理解PI数组主要记录当前匹配无效时下次的有效偏移位数,避免无效的偏移。
KMP算法充分利用已匹配的信息,避免重复的匹配过程。

计算PI数组需要有效自动机中前缀与后缀的概念。
对于字符串abcd,则d,cd,bcd为其后缀,a,ab,abc为其前缀。

KMP的理解点一在于 PI 数组的求解,二在于利用PI数组进行匹配

PI数组的求解:
算法导论-字符串匹配_第7张图片
算法导论中伪代码如上:
对于PI数组的值可以理解为前后缀的匹配,在上述伪代码中,对于PI[1]=0,是因为一个字符,既无后缀,又无前缀。
从5-10行for循环的迭代开始,while循环搜索k(PI数组)的值,直至找到一个k的值,使得P[k+1]=P[q]。
(由算法导论引理32.5与引理32.6可知,k必定在转移函数PI数组中)
8-9行代码实现了PI值得调整,取得前缀是其最大后缀。(引理32.7)。

KMP算法的精髓如下:
算法导论-字符串匹配_第8张图片
KMP的伪代码如下:
算法导论-字符串匹配_第9张图片
算法导论-字符串匹配_第10张图片
这两段代码思想完全相同,如果和前缀不同就比较前缀的前缀
基于C语言实现如下:

#include
#include
#define MAX_LEN 20
int PI[MAX_LEN];

void Compute_prefix_fun(char P[])
{
    int m,k,q;
    m=strlen(P);
    PI[0]=0;
    k=0;
    for(q=1;qwhile((k>0)&&P[k]!=P[q])
        {
            //q=PI[q];
            k=PI[k];
        }
        if(P[k]==P[q])
            k=k+1;

        PI[q]=k;
    }

}
void  KMP_matcher(char T[],char P[])
{
    int n,m,q,i;
    n=strlen(T);
    m=strlen(P);
    Compute_prefix_fun(P);
    q=0;
    for(i=0;iwhile((q>0)&&P[q]!=T[i])
        {
            q=PI[q-1];
        }
        if(P[q]==T[i])
            q=q+1;
        if(q==m)
        {
            printf("%d\n",i-m+1);
            q=PI[q-1];
        }

    }

}
int main()
{


    char T[20],P[20];
    scanf("%s",T);
    scanf("%s",P);
    KMP_matcher(T,P);
    return 0;
}

你可能感兴趣的:(算法)