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
KMP演算法
KMP演算法是一種改進的演算法,由D.E.Knuth,J.H.Morris和V.R.Pratt同時發現,因此人們稱它為KMP演算法。KMP演算法的關鍵是利用匹配失敗後的資訊,儘量減少模式串與主串的匹配次數以達到快速匹配的目的。
演示
如上圖所示,母串“abcxabcdabxabcdabcdabcy”,子串“abcdabcy”。
開始的匹配方法與暴力匹配一致。
如上圖所示,母串的x與子串的d不匹配,KMP演算法不是讓i回到匹配開始的位置,j回到子串開始的位置
如上圖所示,經歷了第一次匹配失敗後,i和j分別來到了x和c。在子串中,c前面的字串為“abcdab”,即相同前後綴“ab”,這就表示在母串中同樣有一段字串“abcdab”,這就意味著,子串中c前面的字串的字首與母串中x前面的字串的字尾所匹配,不用再進行比較,i原地不動,j回到字首“ab”後的c。如果採用暴力演算法,i回到匹配開始的後一位b,j回到子串開始,發現都不匹配,直到子串的a和母串的a匹配,也就是子串的字首匹配到母串的字尾,KMP演算法省時間就體現在這裡。
如上圖所示,x與c不匹配,c前面的字串“ab”也沒有相同前後綴,i不動,j回到子串的開始。
a與x不匹配,i++往後走一位。
如上圖所示,d和y不匹配後,y前面的字串“abcdabc”有相同的前後綴“abc”,所以d前面字串也有同樣的字串,這樣無需比較母串中的字尾和子串中的字首,j回到“abc”後面的d即可,這時發現從d開始後面的字元一一匹配,至此子串在母串的位置被找到了。
那麼現在的問題就是,如何找子串中相同的前後綴呢?並且用什麼樣的資料結構把前後綴資訊給儲存下來?接下來我們繼續。
這裡有一子串“abcdabca”,我們用一個nxt陣列來儲存前後綴資訊,定義i在開始位置,j在第二個位置。
a與b不匹配,nxt[j]=0,j++往後走一位,c與a也不匹配,nxt[j]=0,j++再往後走一位,以此類推。
直到j走到a,a與a匹配,nxt[j]的值是i的值+1,此時i在開始位置,下標為0,所以nxt[j]=0+1=1,表示字串從開始到j這個位置,存在最長公共前後綴的長度為1,然後i,j分別往後各走一位。b與b匹配,nxt[j]=1+1=2,i、j分別往後再走一位,以此類推。
i來到了d,j來到了a,d與a不匹配,i要回到i-1所對應的nxt[i-1]的值(下一個例子會進一步解釋這個原因)。這裡i-1是c,nxt陣列儲存的值是0,i就要回到0這個位置,
這時a與a匹配,nxt[j]=0+1=1。
下一個例子,字串“aabaabaaa”,首先按照之前的思路把各自所對應的nxt陣列的值填好,我們直接來看當i來到b,j來到a
b與a不匹配的時候,是如何處理的。
正如前面所說,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演算法。
定義i、j分別位於母子串的起始位置,如果匹配,i、j各往後一位。這時如圖所示,i到x,j到c,x與c不匹配,j要回到j-1所在的nxt陣列的值,這裡是0,所以j=0。
x與a不匹配,由於j在起始位置,所以i往後走一位,接著開始匹配。
直到,i到c,j到y,c與y不匹配,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)
這裡暫時沒想明白,在以後的更新中會再說明。