首先大体的学习一下有限自动机字符匹配算法,而后在讨论KMP算法。算法
一个有限自动机M是一个五元组(Q,q0,A,Σ,δ),其中:数组
下面定义几个相关函数:函数
来回顾一下朴素算法。给定下面两个字符串,模式串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。这样在自动机的操做中,若是每次状态转移都可以保证:spa
φ(Ti)=σ(Ti)blog
那么就能够保证最终的正确匹配。下面来作简单的推理:递归
根据φ(x)的定义,有φ(Tia) = δ(φ(Ti),a),其中a为任意字母;字符串
由φ(Ti)=σ(Ti),能够获得φ(Tia)=σ(Tia) = q,即φ(Tia)=σ(Pqa);get
综上,δ(φ(Ti),a)=σ(Pqa),,能够获得一个状态转移函数δ(q,a)=σ(Pqa)。这样就能够作出一个正确的状态转移图,而后就能够匹配字符串了。input
用文字来描述一下:在自动机中,状态q就是Ti的后缀在P的最长前缀的长度。这样每次可以知足这个条件,就可以保证算法的正确进行。这里,在《算法导论》中有详细的数学证实。
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的后缀加一个字母是否匹配。此时有两种状况:
下面是C代码的实现的求next数组:
void get_next(char *P, int next[],int len) { printf("len=%d\n",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\n",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\n",j,i); } if(j == len_P-1){ printf("在%d处开始匹配\n",i-len_P+1); j = next[j]; } } } int main() { printf("input the string P:\n"); scanf("%s",P); printf("input the string T:\n"); scanf("%s",T); printf("%s\n",P); get_next(P,next,strlen(P)); int i; for(i = 0; i < strlen(P); i++) { printf("(%d)",next[i]); } printf("\n"); KMP(P,T); return 0; }
可能,中间有些表述不清,求指正哈!
-end-