• 字符串匹配


    首先大致的学习一下有限自动机字符匹配算法,然后在讨论KMP算法。

    有限自动机

    一个有限自动机M是一个五元组(Q,q0,A,Σ,δ),其中:

    • Q是状态的集合,
    • q0∈Q是初始状态,
    • A是Q的字集,是一个接受状态集合,
    • Σ是一个有限的输入字母表,
    • δ是一个从Q×Σ到Q的函数,叫做转移函数。

    下面定义几个相关函数:

    • φ(w)是M在扫描字符串w后终止时的状态。函数φ有下列递归关系定义:φ(ε) = q0,φ(wa) = δ(φ(w),a),
    • σ(x)是x的后缀中,关于P的最长前缀的长度。

    字符串匹配自动机

    来回顾一下朴素算法。给定下面两个字符串,模式串P,和匹配串T。

    i 0 1 2 3 4 5 6 7 8 9 10
    P a b a b a c a        
    T a b a b a b a c a b a

    当第一次匹配时,i=0,但是扫描到i=5的时候,字符串不在匹配。此时另i=1,重新匹配。这就是朴素算法需要改进的地方。当i=5的时候,观察表格发现P[0...3]=T[2...5],此时如果能够匹配T[5+1]和P[3+1]就不需要从i=2开始扫描了,效率就大大的提升了,这样匹配的时间复杂度就只有O(n)了。这里P[0...3]叫做P的前缀,T[2...5]叫做T5的后缀。此时σ(T5) = 3。这样在自动机的操作中,如果每次状态转移都能够保证:

            φ(Ti)=σ(Ti)

    那么就可以保证最终的正确匹配。下面来做简单的推理:

    根据φ(x)的定义,有φ(Tia) = δ(φ(Ti),a),其中a为任意字母;

    由φ(Ti)=σ(Ti),可以得到φ(Tia)=σ(Tia) = q,即φ(Tia)=σ(Pqa);

    综上,δ(φ(Ti),a)=σ(Pqa),,可以得到一个状态转移函数δ(q,a)=σ(Pqa)。这样就可以做出一个正确的状态转移图,然后就可以匹配字符串了。

    用文字来描述一下:在自动机中,状态q就是Ti的后缀在P的最长前缀的长度。这样每次能够满足这个条件,就能够保证算法的正确进行。这里,在《算法导论》中有详细的数学证明。

    KMP算法

    KMP算法不建立一个有限自动机,但是必须要构建一个前缀函数,这里就叫做前缀数组吧。模式P和自己先匹配,得到前缀数组。前缀数组其实保存的就是自动机中的σ(x)的值。这样预处理的时间复杂度和自动机比就减少了很多。

    预处理

    给定模式P:

    i 0 1 2 3 4 5 6 7 8 9
    P a b a b a b a b c a
    next 0 0 1 2 3 4 5 6 0 1

    这里Pi[next[i]]表示的是Pi的关于P的最长后缀,P[i]表示P关于Pi的前缀。

    当i=0时:

    P0和P比较,P0[0] != P[0],所以next[0]=0;

    当i=1时:

    P1和P比较,P1[0] != P[1],所以next[1]=0;

    当i=2时:

    P2和P比较,P2[0] = P[2],所以next[2]=1;

    当i=3时:

    P3和P比较,P3[1] = P[3],所以next[3] = 2;

    如此这般,就可以求得next数组了。一般算法描述数组都是从1开始,但是写代码的时候,数组是从下标0开始的,所以上面的next数组的每一个值都应该减一。next[i]=-1表示没有前缀匹配。这样在写代码的时候,应该是这样的:

    i 0 1 2 3 4 5 6 7 8 9
    P a b a b a b a b c a
    next -1 -1 0 1 2 3 4 5 -1 0

    当i=0时,初始化next[0] = -1;

    当i=1时,(P1[next[0]+1] = a) != (P[1] = b),next[1] = -1;

    当i=2时,(P2[next[1]+1] = a) != (P[2] = a),next[2] = 0;

    当i=3时,(P3[next[2]+1] = b) !=(P[3] = b),next[3] = 1;

    ...

    这样就不难发现next数组的作用了,记录了当前的σ(Pi)。Pi[next[i]+1] = P[i],就表示Pi最长前缀加一个字母和P的后缀加一个字母是否匹配。此时有两种情况:

    • Pi[next[i]+1] = P[i],这个时候σ(Pi+1) = σ(Pi) + 1,继续
    • Pi[next[i]+1] != P[i],这个时候没有直接从Pi[0]是不是等于P[i]扫描,而是从Pi[next[next[i]]+1]开始扫描。因为目前一定可以保证Pi[next[next[i]]是P的一个前缀。

    下面是C代码的实现的求next数组:

    void get_next(char *P, int next[],int len)
    {
    	printf("len=%d
    ",len);
    	next[0] = -1;
    	int q = -1;
    	int i;
    	for(i = 1; i < len; i++) {
    		while(q > 0 && P[q+1] != P[i]) { /* 判断P[q+1]适合等于P[i] */
    			q = next[q]; /* 如果不相等, 一直找到满足条件的最长后缀 */
    		}
    		if(P[q+1] == P[i]) q++; /* 如果相等,那么很好,继续... */
    		next[i] = q;
    		
    	}
    }
    

     匹配

    当求出next数组后,就可以进行字符串匹配了。匹配的方法和求next的方法相识。下面是完整的代码:

    /*************************************************************************
        > File Name: KMP.c
        > Author: mr_zys
        > Mail: 247629929@163.com 
        > Created Time: 2014年10月09日 星期四 14时48分30秒
     ************************************************************************/
    
    #include<stdio.h>
    #include<string.h>
    #define maxn 100
    int next[maxn];
    char P[maxn],T[maxn];
    
    void get_next(char *P, int next[],int len)
    {
    	printf("len=%d
    ",len);
    	next[0] = -1;
    	int q = -1;
    	int i;
    	for(i = 1; i < len; i++) {
    		while(q > 0 && P[q+1] != P[i]) { /* 判断P[q+1]适合等于P[i] */
    			q = next[q]; /* 如果不相等, 一直找到满足条件的最长后缀 */
    		}
    		if(P[q+1] == P[i]) q++; /* 如果相等,那么很好,继续... */
    		next[i] = q;
    		
    	}
    }
    void KMP(char *P, char *T)
    {
    	int len_P = strlen(P);
    	int len_T = strlen(T);
    	int j = -1;
    	int i;
    	for(i = 0; i < len_T; i++) {
    		while(j > -1 && T[i] != P[j+1]) {
    			j = next[j];
    		}
    		if(P[j+1] == T[i]) {
    			j++;
    			//printf("%d %d
    ",j,i);
    		}
    		if(j == len_P-1){
    			printf("在%d处开始匹配
    ",i-len_P+1);
    			j = next[j];
    		}
    	}
    }
    int main()
    {
    	printf("input the string P:
    ");
    	scanf("%s",P);
    	printf("input the string T:
    ");
    	scanf("%s",T);
    	printf("%s
    ",P);
    	get_next(P,next,strlen(P));
    	int i;
    	for(i = 0; i < strlen(P); i++) {
    		printf("(%d)",next[i]);
    	}
    	printf("
    ");
    	KMP(P,T);
    	return 0;
    }
    

     可能,中间有些表述不清,求指正哈!

    -end-

  • 相关阅读:
    [转载]Sublime Text 3 搭建 React.js 开发环境
    浏览器缓存之Expires Etag Last-Modified max-age详解
    第16周作业
    第15周作业
    第14周作业
    第13周作业集
    软件工程结课作业
    第13次作业--邮箱的正则表达式
    第12次作业--你的生日
    第11次作业--字符串处理
  • 原文地址:https://www.cnblogs.com/mr-zys/p/4013677.html
Copyright © 2020-2023  润新知