KMP演算法解析與實現
最近學了KMP演算法,學了半天終於學會了,感覺還是挺難理解的,在這裡與大家分享一下我的思考,也方便我自己複習。
下面就正式進入講解環節吧(本文所有字串初始座標為0!)。
首先我們要明確KMP演算法是幹什麼的:我們有兩個字串T與S,現在我們要找到T中為S的子串,求出滿足條件子串數量及位置。這時候我們就要用到KMP演算法。
傳統演算法回顧:
在講解KMP演算法前,我們先看一下傳統方式的查詢。
在字串T與S中各設一個指標i,j=1;依次比對i+1,j+1、i+2,j+2……..
如果發現有不同的,則比對i+1,j=1;
用圖來看一下(加粗為當前比對位置):
T: c1 c2 c3 c4 c5
S: c1
然後
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
然後
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
這時我們發現c3與d3不同,就比對T-c2與S-c1,即:
T: c1 c2 c3 c4 c5
S: __ c1 c2 d3 e4
這樣的比對,掃描T需要O(n)時間,檢查比對需要O(n)時間。
總時間複雜度:O( n^2 ),這樣的複雜度還算快的。
然而與KMP相比,還是太慢了,KMP可以把時間複雜度優化到O(n+m);
下面讓我們來具體看看KMP的實現。
運用next陣列實現跳轉:
先引入兩個概念:
- 字首串:例如abcde,則其字首依次為:a,ab,abc,abcd,abcde
- 字尾串:例如abcde,則其後綴串依次為e,de,cde,bcde,abcde
對於ababa這個目標串(對比串),我們可以發現:
其字首串與字尾串有相同部分!這就是KMP實現的關鍵。
比對abacdabe與ababa時,我們可以看到:
T: a b a c
S: a b a a b a
有第三位置<初始座標為0>上的a與c不同。
當我們將S往後移動時由於第一位與第三位相同,前兩位已經比對過了。
所以後移後絕對不會匹配.
這樣就浪費了大量的時間。
而KMP的比對就會直接跳到:
T: a b a c d a b e
S:——a b a a b a
具體是如何實現的呢?我們引入一個F[i]來記錄目標串S的字首串與字尾串的最大相同量。
例如:a b a a b a
則對於未知i,有:
i=1,a,F[1]=無;
i=2,a b,F[2]=無;
i=3,a b a,F[3]=1;
i=4,a b a a,F[4]=1;
i=5,a b a a b,F[5]=2;
i=6,a b a a b a,F[6]=3;
但為了方便計數(等會你就知道為什麼了),也方便區分無相同字首的位置,我們將所有F減1,即無相同字首的為-1;
所以:
F[1]=-1;F[2]=-1;F[3]=0;F[4]=0;F[5]=-1;F[6]=2;
下文中所有的表示都是這樣的。
然後我們用幾個例子看一看next是如何實現跳轉的。
例如匹配abaaddddd與abaaba,則用字首陣列的方法為:
字首陣列:a -b a a -b a(‘-‘方便對齊)
———— -1 -1 0 0 1 2
匹配到:
T: a b a a d
S: a b a a b a
F[5-1]=0,有一個相同的字首,j直接跳轉到F[j-1]+1=1(注意初始下標為0);即:
T: a b a a d d d d d
S: ——–a b a a b a
此時匹配位置前一個的’a’已經匹配,實現跳轉。再看一個:
匹配abaabdddd與abaaba,匹配到:
T: a b a a b d d d d
S: a b a a b a
此時F[6-1]=1,有1+1=2個相同字首,j跳轉到F[j-1]+1=2,即:
T: a b a a b d d d d
S: ——–a b a a b a
讀者們可以仔細體會一下上面兩個例子,這裡我給出跳轉的總結:
if(t[i]!=s[j])
{
if(j==0)i++; //沒有相同字首,直接向後移(與一般方法相同)
else j=F[j-1]+1; //跳轉,KMP
}
求解next陣列:
知道如何用next陣列實現跳轉後,我們還要想辦法實現next陣列的求解。
對於next陣列,我們求解的方法與上文其實差不多,但是用到了一個思想:自己匹配自己!
我們先思考:求解next陣列肯定要用O(m)的演算法吧。(要保證KMP的時間複雜度)。
這裡的思路是一個遞推思路。
結合下面的程式碼讀者們不妨先想一想,什麼樣的情況下F[i]可以由前面的F[k]加1求得。
先給出求解F陣列的程式碼:
void get_low()
{
len=t.length();
F[0]=-1;
int temp=0,i;
for(i=1;i<=len-1;i++) //初始下標為0!
{
int j=F[i-1];
while(t[j+1]!=t[i]&&j>=0)j=F[j];
if(t[j+1]==t[i])F[i]=j+1;
else F[i]=-1;
}return;
}
還是先從例子開始吧。
所求字首陣列:
S: a _b a a b _b a b a a b
F: -1 -1 0 0 1 -1 0 1 2 3 4
我們求解第二個:
S:a __b a a b
F:-1 -1
此時這個a的前一個b的F值為-1,所以此時的a不能由b的F[1]求出,j=-1;
比對S[j+1]=a,與當前位a相同,所以F[i]=F[2]=j+1=0;
我們求解第五個:
S= a b a a b b
F=-1 -1 0 0
此時j=F[i-1]=F[4]=0,但當前位置b與前一個a不同,無法接上,j=F[j]=0;
然後比對S[j+1]=S[2]=’b’與當前位b相同,F[i]=F[5]=j+1=1;
這一部分雖然程式碼很短,但這是KMP的難點,比較難理解(但一定不能背代 碼),希望讀者們進行一次完整的模擬以助於理解(對照上文的程式碼),這裡給出一個典型的例子以供參考:
–典型例子:S: a _b a a b _b a b a a b //下標線為方便對齊
–對應next:F:-1 -1 0 0 1 -1 0 1 2 3 4
這個例子把next的所有情況都包含到了,希望讀者們一定要耐著性子模擬一遍。
理解了以上內容後(我承認有點難度),KMP演算法的實現應該也就不難了,下面我給出完整的程式碼<內含用next匹配的具體程式碼>:
模板題來自於:洛谷 P3375 【模板】KMP字串匹配
題目描述
如題,給出兩個字串s1和s2,其中s2為s1的子串,求出s2在s1中所有出現的位置。
為了減少騙分的情況,接下來還要輸出子串的字首陣列next。
(如果你不知道這是什麼意思也不要問,去百度搜[kmp演算法]學習一下就知道了。)
輸入輸出格式
輸入格式:
第一行為一個字串,即為s1(僅包含大寫字母)
第二行為一個字串,即為s2(僅包含大寫字母)
輸出格式:
若干行,每行包含一個整數,表示s2在s1中出現的位置
接下來1行,包括length(s2)個整數,表示字首陣列next[i]的值。
輸入輸出樣例
輸入樣例#1:
ABABABC
ABA
輸出樣例#1:
1
3
0 0 1
說明
時空限制:1000ms,128M
設s1長度為N,s2長度為M
對於100%的資料:N<=1000000,M<=1000
完整程式碼(即KMP演算法的模板):
#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
char t[10000002],s[10002]; //原串、模式串
int F[10002]; //字首next陣列
int len,len1;
inline void get_bef() //求解next字首陣列
{
int i,j;
F[0]=-1;len=strlen(s);
for(i=1;i<=len-1;i++)
{
j=F[i-1];
while(j>=0 && s[i]!=s[j+1])j=F[j];
if(s[i]!=s[j+1])F[i]=-1;
else F[i]=j+1;
}return;
}
inline void find_pos() //求解位置
{
int i,j;
len1=strlen(t);j=0;i=0;
while(i<=len1-1)
{
if(t[i]==s[j]) //這裡為正常匹配
{
i++;
j++;
if(j==len)printf("%d\n",i-j+1);
}
else //KMP跳轉
{
if(j==0)i++;
else j=F[j-1]+1;
}
}return;
}
int main()
{
scanf("%s",&t);
scanf("%s",&s);
get_bef();
find_pos();
for(int i=0;i<=len-1;i++)printf("%d ",F[i]+1);
return 0;
}
希望我的一點理解感悟能夠幫助到oier們,呵呵呵,謝謝觀看。