字符串匹配 - KMP算法

首先大體的學習一下有限自動機字符匹配算法,而後在討論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。這樣在自動機的操做中,若是每次狀態轉移都可以保證: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算法

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\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-

相關文章
相關標籤/搜索