1. 程式人生 > >KMP演算法——基於Youtube外國小哥講解及其Github上程式碼的理解

KMP演算法——基於Youtube外國小哥講解及其Github上程式碼的理解

前言

本篇文章是在看了CSDN上那些“大佬”們對KMP演算法的長篇大論後仍然看不懂,而在Youtube上看了一外國小哥講解的視訊後有所領悟,同時想給廣大受苦群眾分享外國小哥的講解而寫的文章。

外國小哥關於KMP演算法的Java模板

各位可以直接瀏覽以上網頁去獲取直接資訊,跳過我寫的文章,因為這篇文章主要是把原作者的視訊轉換成文字。

如果您不想看視訊,那麼您就可以看看這篇文章,不過我也不建議您這麼做,因為從原作者獲取資訊是最直接的,通過他人獲取的原作者的資訊也許就變了味兒。

因本人能力有限,不保證文章質量,抱歉!

正文

普通暴力的字串查詢搜尋方法

要求是在母串中找到子串,如果找到返回母串所對應子串首字母的下標,如果找不到返回-1等。

如上圖所示,text代表母串“abcbcglx”,pattern代表子串“bcgl”,下面人工簡單模擬一下暴力演算法。

首先定義i,j在text以及pattern的首字母上,然後依次往後匹配。

1、a與b不匹配,i++往後走一位。

2、b與b匹配,i++j++各往後走一位。

3、c與c匹配,i++j++各往後走一位。

4、b與g不匹配,i回到匹配開始位置(b)的後一位(c)j回到子串的首字母。

......

以此類推。

時間複雜度

假如母串是“abcabcabcabcabcabcabc...abx”,子串是“abx”,這樣每次母串到c的時候與子串的x不匹配,i

往後走,j又回到子串首字母,這樣迴圈下去最差的時間複雜度為O(m*n),m、n是母、子串的長度。毋庸置疑,當字串長度很長的時候,時間複雜度還是相當高的。

KMP演算法

KMP演算法是一種改進的演算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它為KMP演算法。KMP演算法的關鍵是利用匹配失敗後的資訊,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。

演示

如上圖所示,母串“abcxabcdabxabcdabcdabcy”,子串“abcdabcy”。

開始的匹配方法與暴力匹配一致。

如上圖所示,母串的x與子串的d不匹配,KMP演算法不是讓i回到匹配開始的位置,j回到子串開始的位置

,這時要看子串匹配失敗處前面的字串“abc”有沒有相同的字首和字尾(字串前後綴問題這裡不解釋,不會的讀者自行百度),這裡並不存在前後綴,所以i不動,j回到子串開始,隨後會再進一步理解這個例子,讀者不要著急。

如上圖所示,經歷了第一次匹配失敗後,ij分別來到了xc。在子串中,c前面的字串為abcdab,即相同前後綴“ab”,這就表示在母串中同樣有一段字串abcdab,這就意味著,子串中c前面的字串的字首與母串中x前面的字串的字尾所匹配,不用再進行比較,i原地不動,j回到字首“ab”後的c。如果採用暴力演算法,i回到匹配開始的後一位bj回到子串開始,發現都不匹配,直到子串的a和母串的a匹配,也就是子串的字首匹配到母串的字尾,KMP演算法省時間就體現在這裡。

如上圖所示,xc不匹配,c前面的字串“ab”也沒有相同前後綴,i不動,j回到子串的開始。

ax不匹配,i++往後走一位。

如上圖所示,dy不匹配後,y前面的字串abcdabc有相同的前後綴“abc”,所以d前面字串也有同樣的字串,這樣無需比較母串中的字尾和子串中的字首,j回到“abc”後面的d即可,這時發現從d開始後面的字元一一匹配,至此子串在母串的位置被找到了。

那麼現在的問題就是,如何找子串中相同的前後綴呢?並且用什麼樣的資料結構把前後綴資訊給儲存下來?接下來我們繼續。

這裡有一子串“abcdabca”,我們用一個nxt陣列來儲存前後綴資訊,定義i在開始位置,j在第二個位置。

ab不匹配,nxt[j]=0j++往後走一位,ca也不匹配,nxt[j]=0j++再往後走一位,以此類推。

直到j走到aaa匹配,nxt[j]的值是i的值+1,此時i在開始位置,下標為0,所以nxt[j]=0+1=1,表示字串從開始到j這個位置,存在最長公共前後綴的長度為1,然後ij分別往後各走一位。bb匹配,nxt[j]=1+1=2ij分別往後再走一位,以此類推。

i來到了dj來到了ada不匹配,i要回到i-1所對應的nxt[i-1]的值(下一個例子會進一步解釋這個原因)。這裡i-1cnxt陣列儲存的值是0,i就要回到0這個位置,

這時aa匹配,nxt[j]=0+1=1

下一個例子,字串“aabaabaaa”,首先按照之前的思路把各自所對應的nxt陣列的值填好,我們直接來看當i來到bj來到a

ba不匹配的時候,是如何處理的。

正如前面所說,i要回到i-1所對應的nxt[i-1]的值,這裡i-1對應的值是2,所以i要回到2這個位置

i-1所對應的nxt[i-1]的值表示i前面的字串的最長公共前後綴長度是多少,回到nxt[i-1]也就是回到i前面字串最長公共字首的後一位,因為i前面的字串與j前面的字串擁有相同的最長公共前後綴,也就是說i前面字串的最長公共字尾與j前面字串的最長公共字首相同,所以i只需回到i前面字串最長公共字首的後一位開始比較。

最終nxt陣列的結果如上圖所示。

程式碼實現

void getnxt(){
	int i=0,j=1;//兩個指標
	nxt[0]=0;//初始化
	while(j<m){//m是字串長度
		if(str[i]==str[j]) nxt[j++]=++i;//如果前後相同,j對應的nxt陣列的值是i座標+1
		else if(!i) nxt[j++]=0;//如果i在起始點,並且前後不相同,j對應的nxt陣列的值為0
		else i=nxt[i-1];//如果前兩者都不是,i就等於i-1對應的nxt陣列的值
	}
}

接下來我們將看看這樣一個nxt陣列在KMP演算法中到底起什麼樣的作用 

如圖所示,母串“abxabcabcaby”,子串“abcaby”,子串對應的nxt陣列同時也給出了,下面我們手動模擬一遍KMP演算法。
定義ij分別位於母子串的起始位置,如果匹配,ij各往後一位。這時如圖所示,ixjcxc不匹配,j要回到j-1所在的nxt陣列的值,這裡是0,所以j=0

xa不匹配,由於j在起始位置,所以i往後走一位,接著開始匹配。

直到,icjycy不匹配,j要回到j-1所在的nxt陣列的值,這裡是2,j=2,接著進行匹配。

隨後發現,母子串一一匹配,子串在母串中的位置被找到。

程式碼實現

void KMP(){
	int i=0,j=0;//初始化
	while(i<n&&j<m){//n是母串長度,m是子串長度
		if(a[i]==b[j]){
			i++;j++;//如果匹配,i、j同時往後走一位
		}
		else if(!j) i++;//如果不匹配且j在起始位置,i往後走一位
		else j=nxt[j-1];//如果都不是,j就回到j-1對應的nxt陣列的值
	}
    //迴圈結束條件要麼i走到頭,母串訪問完;要麼j走到頭,子串訪問完,說明子串在母串的位置被找到
	if(j==m) printf("%d\n",i-j+1);//返回子串在母串的首位置
	else printf("-1\n");//找不到返回-1
}

時間複雜度

O(n+m)

這裡暫時沒想明白,在以後的更新中會再說明。