【知識點】KMP演算法詳解
KMP演算法
演算法簡介
KMP演算法,即看毛片 \({Knuth-Morris-Pratt}\) 演算法。是由三位電腦科學家 \(D.E.Knuth、J.H.Morris、V.R.Pratt\) 提出的。該演算法可以在 \(O(n+m)\) 的時間複雜度內查詢一個字串在另一個字串中的位置。
KMP演算法的基本原理就是尋找模式串的公共前後綴,以優化時間複雜度。
演算法原理
盜用百度的圖片(推薦閱讀)
首先,假設我們有一個字串和一個需要比對的“模式串”,如圖:
首先,我們一個華(bao)麗(li)的開頭,就是把模式串與子串進行逐位匹配:
但是我們發現第六個字元不匹配。
按照傳統的思路,我們需要將模式串右移一位,然後繼續諸位比較。但是KMP演算法是一個毒瘤
我們發現目前已經匹配的子串中,字首和字尾都是一樣的,都是“GTG”:
所以我們驚訝地發現,我們可以直接把模式串右移到最長可匹配字尾的位置,然後繼續愉快的比較
可是又出現了一個“壞字元”,我們應該如何處理呢?
沒錯,繼續尋找可匹配最長字首和字尾,然後再次移動模式串。
以此類推。這就是KMP演算法的流程。
原理應該理解了,那麼要如何實現呢?
演算法實現
一、\(nxt\) 陣列
\(nxt\) 是一個一維整型陣列,陣列的下標代表了“已匹配字首的下一個位置”,元素的值則是“最長可匹配字首子串的下一個位置”。如圖所示:
其中,由於子串“G”、“GT”、“GTGTGC”沒有可匹配的字首和字尾,所以對應的 \(nxt\)
只要我們求出 \(nxt\) 陣列,我們就可以解決尋找最長可匹配前後綴並移動模式串的問題了。
相信聰明的你已經理解了
二、求出 \(nxt\) 陣列
我們設兩個變數,\(i\) 和 \(j\) ,它們分別表示“已匹配字首的下一個位置”,也就是待填充的陣列下標,和“最長可匹配字首子串的下一個位置”,也就是待填充的陣列元素值。它們的初始值如下:
求出 \(nxt\) 陣列的過程,就是用模式串“自己匹配自己”。
首先,我們讓 \(i++\) 。
此時,一匹配子串長度為 \(1\) ,不存在可匹配字尾,故 \(nxt[1]=0\)。
我們令 \(i\) 繼續 \(+1\)。
可以發現,最長可匹配前後綴子串仍不存在,所以 \(nxt[2]=0\)
當 \(i\) 又一次加一時,我們發現此時的模式串 \(s\) 中存在 \(s[j]=s[i-1]\) ,於是 \(nxt[3]=nxt[2]+1=1\) 。
現在,我們需要讓 \(i,\ j\) 都加一。
我們驚訝的發現,\(s[j]=s[i-1]='T'\) ,所以可匹配最長前後綴子串的長度加一,即 \(nxt[4]=nxt[3]+1=2\)。
當 \(i,\ j\) 再次同時加一後,我們找到了 \(s[j]=s[i-1]='G'\),所以 \(nxt[5]=nxt[4]+1=3\)
可是此時的 \(s[j]\) 和 \(s[i-1]\) 不匹配了,怎麼辦呢?
按照套路,應該移動模式串了。如何移動?簡單地移動一位嗎?當然不,我們令 \(j=nxt[j]\) 。
可是天不從人願,我們發現字元仍然不匹配。所以再次使 \(j=nxt[j]\) 。
此時, \(j\) 已經無法回溯,所以 \(nxt[6]=0\), \(nxt\) 陣列就求出來了。
推導完畢。匹配的過程也類似。
建議將 \(nxt\) 陣列的推導多看幾遍,這樣可以加深理解。因為 \(nxt\) 陣列的推導過程是KMP演算法最反人類核心的地方。
具體程式碼實現
\(\mathtt{Talking\ is\ cheap,\ show\ me\ the\ code.}\)
void KMP(char *s1,char *s2)
{
/* KMP演算法
* @params s1為主串,s2為模式串,l1,l2分別是它們的長度
* @from 程式碼來自Luogu P3375,是一道KMP模板題
*/
int l1=strlen(s1),l2=strlen(s2);
int j=0;
for(int i=2;i<=l2;i++)
{
while(j && s2[i]!=s2[j+1])
j=nxt[j];
if(s2[i]==s2[j+1]) j++;
nxt[i]=j;
}
j=0;
for(int i=1;i<=l1;i++)
{
while(j && s1[i]!=s2[j+1])
j=nxt[j];
if(s1[i]==s2[j+1]) j++;
if(j==l2)
printf("%d\n",i-l2+1),j=nxt[j];
}
}
注:\(Galax OJ\) 的 \(KMP\) 練習題題解已經在路上啦~