天天看點

詳解KMP入門

在網上很容易找到分析KMP算法的部落格,但我覺得他們都沒有講到點子上去,導緻一些讀者越看越困惑,我則是看了劉汝佳的書後,自我比較通俗易懂的總結一下。

解決問題:給出字元串T和P,求P在T中第一次出現的位置,簡稱字元串比對。

算法準備:最單純的模拟算法就是每次枚舉起始點,再判斷是否比對,效率O(nm),極低。

                  但根據經驗,任何一個高效算法的提出就是減少的備援狀态,在模拟過程中,很明顯做了一次又一次重複的事情,我們應該想辦法避免重複。

分析:

T=‘ABRACADABRACABRACABRAE’

P=‘ABRAE’

首次比對,發現T[4]='C',P[4]='E',導緻比對失敗,簡稱失配,

然後根據模拟算法,接着我們又從T[1],P[0]開始重新比對,但是明顯我們沒有用到第一次比對時得到的資訊,其實第一次比對時,我們就知道T[1]和P[1]相等,T[0]和P[0]相等,如果我們能提前處理出P[1]與P[0]是否相等,我們就可以判斷是接着比對T[2]和P[1],還是重新開始下一個輪T[2]與P[0]的比對。

是以,我們有了一個思路:

對于字元串P,提前預處理出f[i]表示目前P[i]與前面哪個j對應的P[j]相等。

但很快我們發現,這在最壞情況下這并不能使效率提升多少。

再次深度分析:

當我們一次比對進行到P[j],T[i],而P[j]不等于T[i],導緻失配,但我們已經已經知道P[j-1]等于T[i-1],我們可以在P中直接找與P[j-1]等效的地方P[k]重新開始比對T[i-1],顯然k<j,

若k>j,P[j]與T[i]并不相等,是以一定失配。

顯然f數組中仍儲存了大量的備援資訊

那麼什麼是等效? 等效的P[j]和P[k]即指P(1...k)=P(j-k+1....j)

是以我們可以預處理出f[j]表示與P[j]等效的P[k]的位置,那麼失配後,

我們在j前面找到與P[j-1]等效的地方P[k],同時P[k+1]=T[i]。然後接着比對P[k+2]與T[i+1],如果找不到對應的k,顯然此次比對是失敗的,需要從頭開始。

這就是KMP算法,很簡單吧?但同時似乎效率并不高,因為有兩層循環,且第二層在極端情況下好像是會退化的,真的是這樣麼?

可以這樣計算時間複雜度:每一次比對成功都會有j++,i++,而每一次j=f[j](即找到前一個與P[j-1]相等的地方)都會使j最少-1,但由于j最多隻會增加n次,是以j=f[j]的次數最多也隻有n次。是以效率為O(n)。

參考程式:

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=11000;
int f[maxn];
char P[maxn],T[maxn];
int n,m;
void getfail(char* P,int* f){
	f[0]=0;f[1]=0;
	for (int i=1;i<m;i++){
		int j=f[i];
		while (j && P[i]!=P[j])j=f[j];
		f[i+1]=P[i]==P[j]?j+1:0;
	}
}
int find(char* P,char* T,int* f){
	int j=0;
	for (int i=0;i<n;i++){
		while (j && T[i]!=P[j])j=f[j];
		if (P[j]==T[i])j++;
		if (j==m)return i-m+1;
	}
	return -1;
}
int main(){
	scanf("%d%d",&n,&m);
	scanf("%s%s",T,P);
	getfail(P,f);
	printf("%d\n",find(P,T,f));
	return 0;
}