1. 程式人生 > >KMP演算法簡解:兩張圖徹底看懂

KMP演算法簡解:兩張圖徹底看懂

網上有很多關於講解KMP演算法的文章,很多都用了具體的例子,但本文只需要兩張抽象圖,即可快速理解KMP演算法。

在理解了BF演算法之後,我們發現因為模式串指標的每次復位,都可能造成不必要的某段匹配,這就需要某種策略,來使模式串指標跳過這段匹配,KMP演算法應運而生。以下以一張圖來解釋KMP演算法的基本原理(改自“資料結構(C++語言版),鄧俊輝著”):

如圖,KMP基本策略是當文字串T與模式串P在i位置處失配,則指標j用t來替代,即失配字元P[j]用P[t]來替代, z繼續與x比較,比較結果目前未知,但可以確定的是,z之前的字串是不必比較的,也即S1=S3。此策略,相當於指標i不動,P向右滑動了一段距離,使z與x對齊,而滑動距離顯然是j-t。顯然,S3和S2都是P的前t個字元組成的串,則S1=S2=S3,於是S2可作為S4的字首,S3可作為S4的字尾,而指標j已知,於是滑動距離j-t只取決於S4相等的前後綴的長度t,而與T無關。

S4可能有多個相等的前後綴,也即S4可以有多個t,如何在S4中選取最佳的t呢?實驗表明,t取最大時最佳,也就是滑動距離j-t最小時,指標i不可能回溯,也不可能遺漏任何可能的匹配。於是我們將在P每次失配時的子串S4中取最佳的t組成一張表,即用一個稱作next的陣列來儲存,當P的j位置失配,j就可用next[j]來替代,繼續與T[i]比較,於是KMP演算法可描述如下:

int KMP(char* P,char* T){
	int n=strlen(T),i;//文字串,指標 
	int m=strlen(P),j;//模式串,指標 
	int* next=bulidNext(P);//獲得next表 
	while(i<n&&j<m){
		if(j<0||T[i]==P[j]) 
		{i++;j++;} /*
若P已移出左側或當前字元匹配成功,則移動字元
注意因為括號中判定順序是自左向右,故判定順序不可交換,必須先判定j<0,
否則P[j]可能越界訪問從而出錯
*/
		else
			j=next[j];//從next陣列中獲取下一位置,注意,i無需回溯 
	}
	delete []next;
	return i-j;
}

相較於BF演算法,i無需回溯,j需要判定是否為負,後者正是基於next陣列的特性,當P在j=0處失配時,P[0,j),不妨沿用之前的名字S4,S4之前已沒有任何字元可選作新的比較字元,故可假想地在S4之前設定一個哨兵P[-1],該字元與文字串任一字元都匹配,於是j=p[j]=-1,P[j]與文字串中的失配字元匹配,於是i和j指標統一向前移動一個單位。換句話說,當P在j=0處失配說明已經匹配到頭了,需要在失配字元的下一位置重新開始比較。

既然next[0]=-1,那麼next陣列的其他值又如何計算呢?首先,next陣列的值是P每次失配時S4的相等前後綴的最大長度,這個求相等前後綴的過程,其實就是S4自身的前後綴自匹配的過程,若匹配成功則前後綴相等,而S4是P的一個子串,故求next陣列,就是求不斷地對P進行自匹配的過程,於是可畫出下圖:

如上圖,當P在j處失配,則可像P與T匹配時那樣通過next[j]來獲取下一次匹配位置,以下前後綴最大長度簡稱最大長度。獲得next[j]後,對比第1張圖,它就是S的最大長度t,而若P在j+1處失配,則需從S再加一個字元x組成的字串中取最大長度next[j+1],而此長度最多是S的最大長度next[j]再+1,也就是說,next[j+1]<=next[j]+1。當此式取等號時,next[j+1]也就是S的最大長度t+1。此時P[j]與P[t]相等,即z歸入S2。

當P[j]與P[t]不等時,如何求next[j+1]?顯然,此時相當於t失配,則再次通過next陣列獲得next[t],若next[t]==t,則next[j+1]=next[t]+1=next[next[j]]+1,若next[t]!=t,則按上述方法以此類推,即next[j+1]=next[…next[j]…]+1,直到P[j]==P[t]的情況出現,包括t=-1的情況。

總的來說,對於位置P[j+1]之前的串中的最大前後綴長度會因j位置的失配不斷減少,過程中不斷令j=next[j],直到有一個字元與P[j]匹配,那麼next[j+1]隨即確定,這樣便確定了一個位置上的next值,而要確定所有位置上的next值,需讓j從0開始一直判定到m-2,m-2的next值確定,m-1的next值也隨即確定,構造next陣列的演算法可描述如下:

int* bulidNext(char*P){
	int m=strlen(P),j=0;//j指示模式串位置 
	int *N=new int[m],t=-1;//t指示next陣列位置,也可理解為指示覆制模式串位置 
	N[0]=-1;//初始化,N[0]=-1
	while(j<m-1){//因為始終有N[j+1]=t+1,故j只需不大於j-2即可 
		if(t<0||P[j]==P[t]){//若t回溯到-1或者匹配 
			j++;t++;
			N[j]=t;//N[j+1]=N[j]+1=t+1 
		}
		else t=N[t];//若失配,按照模式匹配那樣從next陣列中獲取 
	}
	return N;
}

上述演算法看似完美,然而還有待改進的地方,因實驗表明,當p[j]==p[t]時,若p[j+1]==p[t+1],會增加多次註定失敗的比對,故可在p[j]==p[t]時,在j和t自增後,再次判定p[j]!=p[t],若成立,則正常賦值,若不成立,再令t=next[t]。具體做法是將原語句“N[j]=t”改為“N[j]=(P[j]!=P[t]?t:N[t])”