最近在刷題的時候遇到了KMP 算法的一些題目,回想起來自己關於數據結構和算法的知識內容都已經忘了。於是打算重新複習一遍。就從KMP算法開始吧。因爲自己水平還不夠,也講不清楚。於是就從網上轉載了兩篇博文。
原文鏈接 https://blog.csdn.net/v_JULY_v/article/details/6545192
出處 http://blog.csdn.net/v_JULY_v 。
KMP 構建next數組的數學證明 (最好弄懂構建next數組的原理和數學證明)
原文鏈接 http://blog.renren.com/share/233564479/621540832/1
作者:濱湖,July、yansha。
說明:初稿由濱湖提供,July負責KMP部分的勘誤,yansha負責BM部分的修改。全文由July統稿修訂完成。
出處:http://blog.csdn.net/v_JULY_v 。
引言
在此之前,說明下寫作本文的目的:1、之前承諾過,這篇文章六、教你從頭到尾徹底理解KMP算法、updated之後,KMP算法會寫一個續集;2、寫這個kMP算法的文章很多很多,但真正能把它寫明白的少之又少;3、這個KMP算法曾經困擾過我很長一段時間。我也必須讓讀者真真正正徹徹底底的理解它。希望,我能做到。
ok,子串的定位操作通常稱做串的模式匹配,是各種串處理系統中最重要的操作之一.在很多應用中都會涉及子串的定位問題,如普通的字符串查找問題.如果我們把模式匹配的串看成一字節流的話,那應用空間一下子就廣闊了很多,如HTTP協議裏就是字節流,有各種關鍵的字節流字段,對HTTP數據進行解釋就需要用到模式匹配算法.
本文是試圖清楚的講解模式匹配算法裏兩個最爲重要的算法:KMP與BM算法,這兩個算法都較爲高效,特別是BM算法在工程用應用得非常多的,然而網上很多BM算法都不算準確的。本文開始講解簡單回溯字符串匹配算法,後面過渡到KMP算法,最後再過渡到BM算法,希望能夠講得明白易懂。
模式匹配問題抽象爲:給定主串S(Source,長度爲n),模式串P(Pattern, 長度爲m),要求查找出P在S中出現的位置,一般即爲第一次出現的位置,如果S中沒有P子串,返回相應的結果。如下圖0查找成功,則查找結果返回2:
圖0 字符串查找
本文,接下來,將一步一步講解KMP算法。希望看完本文後,讀者日後對Kmp算法能做到胸中丘壑自成。文章有任何錯誤,煩請一定指出來。謝謝。
第一部分、KMP算法
- 1、回溯法字符串匹配算法
回溯法字符串匹配算法就是用一個循環來找出所有有效位移,該循環對n-m+1個可能的位移中的每一個index值,檢查條件爲P[0…m-1]= S[index…index+m-1](因爲模式串的長度是m,索引範圍爲0…m-1)。
S 0......index.... index+m-1 (src[i]表示)
P 0 .... m-1 (patn[j]表示)
- //代碼1-1
- //int search(char const*, int, char const*, int)
- //查找出模式串patn在主串src中第一次出現的位置
- //plen爲模式串的長度
- //返回patn在src中出現的位置,當src中並沒有patn時,返回-1
- int search(char const* src, int slen, char const* patn, int plen)
- {
- int i = 0, j = 0;
- while( i < slen && j < plen )
- {
- if( src[i] == patn[j] ) //如果相同,則兩者++,繼續比較
- {
- ++i;
- ++j;
- }
- else
- {
- //否則,指針回溯,重新開始匹配
- i = i - j + 1; //退回到最開始時比較的位置
- j = 0;
- }
- }
- if( j >= plen )
- return i - plen; //如果字符串相同的長度大於模式串的長度,則匹配成功
- else
- return -1;
- }
在繼續分析之前,咱們來思考這樣一個問題:爲什麼快排或者堆排序比直接的選擇排序快?直接的選擇排序,每次都是重複的比較數值的大小,每掃描一次,只得出一個最大(小值),再沒有其它的結果信息能給下一次掃描帶來便捷。我們看看快排,每掃一次,將數據按某一值分成了兩邊,至少有右邊的數據都大於左邊的數據,所以在比較的時候,下一次就不用比較了。再看看堆排序,建堆的過程也是O(n)的比較,但比較的結果得到了最大(小)堆這種三角關係,之後的比較就不用再每一個都需要比較了。
由上述思考,咱們總結出了一點優化的歸律:採用一種簡單的數據結構或者方式,將每次重複性的工作得到的信息記錄得儘量多,方便下一次做同樣的工作,這樣將帶來一定的優化(個人性總結)。
回溯法做的多餘的工作
以下給出一個例子來啓發,如下圖2:
圖1-1 回溯法的一個示例
可以看出當匹配到g與h的時候,不匹配了(後面,你將看到,KMP算法會直接從匹配失效的位置,即g位置處重新開始匹配,這就是KMP的高效之處),模式串的下一個位置該怎麼移動,需要回溯到第二個位置如:
圖1-2 回溯到第二個位置
在第二個位置發現還是不匹配,便再次回溯到第三個位置:
圖1-3 回溯到第三個位置
其實可以分析一下模式串裏,每個字符都不相同,如果前面有匹配成功,那移動一位或者幾位後,是不可能匹配成功的。
啓示:模式串裏有蘊含信息的,可以簡化掃描。接下來深入的討論另一算法KMP算法。
- 2、KMP算法的簡介
KMP算法就是一種基於分析模式串蘊含信息的改進算法,是D.E.Knuth與V.R.Pratt和J.H.Morris同時發現的,因此人們稱它爲KMP算法。
咱們還是以上面的例子爲例,如下圖2-1:
圖2-1 KMP算法的一個例子
如果是普通的匹配算法,那麼接下來,模式串的下一個匹配將如上一節讀者所看到的那樣,回溯到第二個位置b處。而KMP算法會怎麼做呢?KMP算法會直接把模式串移到匹配失效的位置上,如下圖2-2,g處:
圖2-2 直接移到匹配失效的位置g處
Ok,咱們下面再看一個例子,如下圖2-3/4:
圖2- 3/4 另一個例子
我們爲什麼要這麼做呢?如上面的例子,每個字符都不相同,如果前面有匹配成功,那移動一位或者幾位後,是不可能匹配成功的,所以我們完全可以就模式串的特點來決定下一次匹配從哪個地方開始。
問題轉化成爲對於模式串P,當P[j](0<=j<m)與主串匹配到第i個字符(S[i], 0<=i<n)失敗的時候,接下來應該用什麼位置的字符P[j_next](我們設j_next即匹配失效後下一個匹配的位置)與主串S[i]開始匹配呢?重頭開始匹配?No,在P[j]!=S[i]之前的時候,有S[i-j…i-1]與P[0…j-1]是相同的,所以S不用回溯,因爲S[i]前面的值都已經確切的知道了。
S 0 i-j..i-1 i .... n (S[i]表示,S[i]處匹配失敗)
P 0.. j-1 j.. m (P[j]表示,要找下一個匹配的位置P[j_next])
以上,在P[j]!=S[i]之前的時候,有S[i-j…i-1]與P[0…j-1]是匹配即相同的字符,各自都用下劃線表示。
咱們先寫下算法,你將看到,其實KMP算法的代碼非常簡潔,只有20來行而已。如下描述爲:
- //代碼2-1
- //int kmp_seach(char const*, int, char const*, int, int const*, int pos) KMP模式匹配函數
- //輸入:src, slen主串
- //輸入:patn, plen模式串
- //輸入:nextval KMP算法中的next函數值數組
- int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)
- {
- int i = pos;
- int j = 0;
- while ( i < slen && j < plen )
- {
- if( j == -1 || src[i] == patn[j] )
- {
- ++i;
- ++j; //匹配成功,就++,繼續比較。
- }
- else
- {
- j = nextval[j];
- //當在j處,P[j]與S[i]匹配失敗的時候直接用patn[nextval[j]]繼續與S[i]比較,
- //所以,Kmp算法的關鍵之處就在於怎麼求這個值拉,
- //即匹配失效後下一次匹配的位置。下面,具體闡述。
- }
- }
- if( j >= plen )
- return i-plen;
- else
- return -1;
- }
- 3、如何求next數組各值
現在的問題是p[j_next]中的j_next即上述代碼中的nextval[j]怎麼求。
當匹配到S[i] != P[j]的時候有 S[i-j…i-1] = P[0…j-1]. 如果下面用j_next去匹配,則有P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。此過程如下圖3-1所示。
當匹配到S[i] != P[j]時,S[i-j…i-1] = P[0…j-1]:
S: 0 … i-j … i-1 i …
P: 0 … j-1 j …
如果下面用j_next去匹配,則有P[0…j_next-1] = S[i-j_next…i-1] = P[j-j_next…j-1]。
所以在P中有如下匹配關係(獲得這個匹配關係的意義是用來求next數組):
P: 0 … j-j_next .…j-1_ …
P: 0 … .j_next-1 …
所以,根據上面兩個步驟,推出下一匹配位置j_next:
S: 0 … i-j … i-j_next … i-1 i …
P: 0 … j_next-1 j_next …
圖3-1 求j-next(最大的值)的三個步驟
下面,我們用變量k來代表求得的j_next的最大值,即k表示這S[i]、P[j]不匹配時P中下一個用來匹配的位置,使得P[0…k-1] = P[j-k…j-1],而我們要儘量找到這個k的最大值。如你所見,當匹配到S[i] != P[j]的時候,最大的k爲1(當S[i]與P[j]不匹配時,用P[k]與S[i]匹配,即P[1]和S[i]匹配,因爲P[0]=P[2],所以最大的k=1)。
圖3-2 j_next=1,即最大的k的值爲1
如上圖3-2,當P[3]!=S[i],而P[0]=P[2](當P[3]!=S[i],而P[0]=P[2],P[2]=S[i-1],所以肯定有P[0]=S[i-1])),所以只需比較P[1]與S[i]就可以了,即k是P可以跳過比較的最大長度,換句話說,就是k能標示出S[i]與P[j]不匹配時P的下一個匹配的位置。
圖3-3 第二步匹配中,跳過P[0](a),只需要比較 P[1]與S[3](b)了
也就是說,如上圖3-3,在第一次匹配中,就是因爲S[2]=P[0],所以在下一次匹配中,只需要比較S[3]=P[1],跳過了幾步?一步。那麼k等於多少?k=1。即把 P 右移兩個位置後,P[0]與S[2]不必再比較,因爲前一步已經得出他們相等。所以,此時,只需要比較 P[1]與S[3]了。
接下來的問題是,怎麼求最大的數k使得p[0…k-1] = p[j-k…j-1]呢。這就是KMP算法中最核心的問題,即怎麼求next數組的各元素的值?只有真正弄懂了這個next數組的求法,你才能徹底明白KMP算法到底是怎麼一回事。
那麼,怎麼求這個next數組呢?咱們一步一步來考慮。
求最大的數k使得P[0…k-1] = P[j-k…j-1],一個直接的辦法是對於j,從P[j-1]往回查,看是否有滿足P[0…k-1] = P[j-k…j-1]的k存在,而且還要最大的一個k。下面咱們換一個角度思考。
當P[j+1]與S[i+1]不匹配時,分兩種情況求next數組(注:以下皆有k=next[j]):
- P[j] = p[k], 那麼next[j+1]=k+1,這個很容易理解。採用遞推的方式求出next[j+1]=k+1(代碼3-1的if部分)。
- P[j] != p[k],那麼next[j+1]=next[k]+1(代碼3-1的else部分)
稍後,你將看到,由這個方法得出的next值還不是最優的,也就是說是不能允許P[j]=P[next[j]]出現的。ok,請跟着我們一步一步登上山頂,不要試圖一步登天,那是不可能的。由以上,可得如下代碼:
- //代碼3-1,稍後,你將由下文看到,此求next數組元素值的方法有錯誤
- void get_next(char const* ptrn, int plen, int* nextval)
- {
- int i = 0;
- nextval[i] = -1;
- int j = -1;
- while( i < plen-1 )
- {
- if( j == -1 || ptrn[i] == ptrn[j] ) //循環的if部分
- {
- ++i;
- ++j;
- nextval[i] = j;
- }
- else //循環的else部分
- j = nextval[j]; //遞推
- }
- }
上述求next數組各值的方法(代碼)是否正確呢?我們來舉一個例子,應用上述的get_next函數來試驗一下,即具體求解一下next數組各元素的值(通過下面的驗證,我們將看到上面的求next數組的方法是有問題的,而後我們會在下文的第4小節具體修正上述求next數組的方法)。ok,請看:
首先,模式串如下:字符串abab下面對應的數值即是已經求出的對應的nextval[i]值:
圖3-4 求next數組各值的示例
接下來,咱們來具體解釋下上面next數組中對應的各個nextval[i]的值是怎麼求得來的,因爲,理解KMP算法的關鍵就在於這個求next值的過程。Ok,如下,咱們再次引用一下上述求next數組各值的核心代碼:int i = 0;
nextval[i] = -1;
int j = -1;
while( i < plen-1 )
{
if( j == -1 || ptrn[i] == ptrn[j] ) //循環的if部分
{
++i;
++j;
nextval[i] = j;
}
else //循環的else部分
j = nextval[j]; //遞推
}所以,根據上面的代碼,咱們首先要初始化nextval[0] = -1,我們得到第一個next數組元素值即-1(注意,咱們現在的目標是要求nextval[i]各個元素的值,i是數組的下標,爲0.1.2.3);
圖3-5 第一個next數組元素值-1
首先初始化:i = 0,j = -1,由於j == -1,進入上述函數中循環的if部分,++i得 i=1,++j得j=0,所以我們得到第二個next值即nextval[1] = 0;
圖3-6 第二個next數組元素值0
i= 1,j = 0,由於不滿足條件j == -1 || ptrn[i] == ptrn[j](第一個元素a與第二個元素b不相同,所以也不滿足第2個條件),所以進入上述循環的else部分,得到j = nextval[j] = -1(原來的nextval[0]=-1並沒有改變),得到i = 1,j = -1;此時,由於j == -1且i<plen-1依然成立,所以再次進入上述循環的if部分,++i的i=2,++j得j=0,所以得到第三個next值即nextval[2] = 0;
圖3-7 第三個next數組元素值0
此時,i = 2,j = 0,由於ptrn[i] == ptrn[j](第1個元素和第3個元素都是a,相同,所以,雖然不滿足j=-1的第1個條件,但滿足第2個條件即ptrn[i] == ptrn[j]),進入循環的if部分,++i得i=3,++j得j=1,所以得到我們的第四個next值即nextval[3] = 1(由下文的第4小節,你將看到,求出的next數組之所以有誤,問題就是出在這裏。正確的解決辦法是,如下文的第4小節所述,++i,++j之後,還得判斷patn[i]與patn[j]是否相等,即杜絕出現P[j]=P[next[j]]這樣的情況);
自此,我們得到了 nextval[i]數組的4個元素值,分別爲-1,0,0,1。如下圖3-8所示:圖3-8 第四個next數組元素值1
求得了相應的next數組(本文約定,next數組是指一般意義的next數組,而nextval[i]則代表具體求解next數組各數值的意義)各值之後,接下來的一切工作就好辦多了。
第一步:主串和模式串如下,由下圖可以看到,我們在p[3]處匹配失敗(即p[3]!=s[3])。
圖3-9 第一步,在p[3]處匹配失敗
第二步:接下來要用p[next[3]](看到了沒,是該我們上面求得的next數組各值大顯神通的時候了),即p[1]與s[3]匹配( 不要忘了,上面我們已經求得的nextval[i]數組的4個元素值,分別爲-1,0,0,1)。但在p[1]處還是匹配失敗(即p[1]!=s[3])。
圖3-10 第二步,p[1]處還是匹配失敗
第三步:接下來模式串指針指向下一位置next[1]=0處(注意此過程中主串指針是不動的),即模式串指針指向p[0],即用p[0]與s[3]匹配(看起來,好像是k步步減小,這就是咱們開頭所講到的怎麼求最大的數k使得P[0…k-1] = [j-k…j-1])。而p[0]與s[3]還是不匹配。
圖3-11 第三步,p[0]與s[3]還是不匹配
第四步:由於上述第三步中,P[0]與S[3]還是不匹配。此時i=3,j=nextval[0]=-1,由於滿足條件j==-1,所以進入循環的if部分,++i=4,++j=0,即主串指針下移一個位置,從p[0]與s[4]處開始匹配。最後j==plen,跳出循環,輸出結果i-plen=4(即字串第一次出現的位置)
圖3-12 第四步,跳出循環,輸出結果i-plen=4
所以,綜上,總結上述四步爲:
- P[3]!=S[3],匹配失敗;
- nextval[3]=1,所以P[1]繼續與S[3]匹配,匹配失敗;
- nextval[1]=0,所以P[0]繼續與S[3]匹配,再次匹配失敗;
- nextval[0]=-1,滿足循環if部分條件j==-1,所以,++i,++j,主串指針下移一個位置,從P[0]與S[4]處開始匹配,最後j==plen,跳出循環,輸出結果i-plen=4,算法結束。
不知,讀者是否已看出,上面的匹配過程隱藏着一個不容忽略的問題,即有一個完全可以改進的地方。對的,問題就出現在上述過程的第二步。
觀察上面的匹配過程,看匹配的第二步,在第一步的時候已有P[3]=b與S[3]=c不匹配,而下一步如果還是要讓P[next[3]]=P[1]=b與s[3]=c匹配的話,那麼結果很明顯,還是肯定會匹配失敗的。由此可以看出我們的next值還不是最優的,也就是說是不能允許P[j]=P[next[j]]出現的,即上面的求next值的算法需要修正。
也就是說上面求得的nextval[i]數組的4個元素值,分別爲-1,0,0,1是有問題的。有什麼問題呢?就是不容許出現這種情況P[j]=P[next[j]]。爲什麼?
好比上面的例子。請容許我再次引用上面例子中的兩張圖。在上面的第一步匹配中,我們已經得出P[3]=b是不等於S[3]=c的。而在上面的第二步匹配中,根據求得的nextval[i]數組值中的nextval[3]=1,即讓P[1]重新與S[3]再次匹配。這不是明擺着有問題麼?因爲P[1]也等於b阿,而在第一步匹配中,我們已經事先得知b是不可能等於S[3]的。所以,第二步匹配之前就已註定是失敗的。
圖3-13/14 求next數組各值的錯誤解法
這裏讀者理解可能有困難的是因爲文中,時而next,時而nextval,把他們的思維搞混亂了。其實next用於表達數組索引,而nextval專用於表達next數組索引下的具體各值,區別細微。至於文中說不允許Pj=P[next[j] ]出現,是因爲已經有P3=b與SI匹配敗,而P[next]=P=b,若再拿P[1]去與S匹配則必敗。
- 4、求解next數組各值的方法修正
那麼,上面求解next數組各值的問題到底出現在哪兒呢?我們怎麼才能擺脫掉這種情況呢?:即不能讓P[j]=P[next[j]]成立成立。不能再出現上面那樣的情況啊!即不能有這種情況出現:P[3]=b,而竟也有P[next[3]]=P[1]=b。
讓我們再次回顧一下之前求next數組的函數代碼:
- //引用之前上文第3小節中的有錯誤的求next的代碼3-1。
- void get_next(char const* ptrn, int plen, int* nextval)
- {
- int i = 0;
- nextval[i] = -1;
- int j = -1;
- while( i < plen-1 )
- {
- if( j == -1 || ptrn[i] == ptrn[j] ) //循環的if部分
- {
- ++i;
- ++j;
- nextval[i] = j; //這裏有問題
- }
- else //循環的else部分
- j = nextval[j]; //遞推
- }
- }
由上面之前的代碼,我們看到,在求next值的時候採用的是遞推。這裏的求法是有問題的。因爲在s[i]!=p[j]的時候,如果p[j]=p[k](k=nextval[j],爲之前的錯誤方法求得的next值),那麼P[k]!=S[i],用之前的求法求得的next[j]==k,下一步直接導致匹配(S[i]與P[k]匹配)失敗。
根據上面的分析,我們知道求next值的時候還要考慮P[j]與P[k]是否相等。當有P[j]=P[k]的時候,只能向前遞推出一個p[j]!=p[k'],其中k'=next[next[j]]。修正的求next數組的get_nextval函數代碼如下:
- //代碼4-1
- //修正後的求next數組各值的函數代碼
- void get_nextval(char const* ptrn, int plen, int* nextval)
- {
- int i = 0;
- nextval[i] = -1;
- int j = -1;
- while( i < plen-1 )
- {
- if( j == -1 || ptrn[i] == ptrn[j] ) //循環的if部分
- {
- ++i;
- ++j;
- //修正的地方就發生下面這4行
- if( ptrn[i] != ptrn[j] ) //++i,++j之後,再次判斷ptrn[i]與ptrn[j]的關係
- nextval[i] = j; //之前的錯誤解法就在於整個判斷只有這一句。
- else
- nextval[i] = nextval[j];
- }
- else //循環的else部分
- j = nextval[j];
- }
- }
舉個例子,舉例說明下上述求next數組的方法。
S a b a b a b c
P a b a b c
S[4] != P[4]
那麼下一個和S[4]匹配的位置是k=2(也即P[next[4]])。此處的k=2也再次佐證了上文第3節開頭處關於爲了找到下一個匹配的位置時k的求法。上面的主串與模式串開頭4個字符都是“abab”,所以,匹配失效後下一個匹配的位置直接跳兩步繼續進行匹配。
S a b a b a b c
P a b a b c
匹配成功P的next數組值分別爲-1 0 -1 0 2
next數組各值怎麼求出來的呢?分以下五步:
- 初始化:i=0,j=-1;
- i=1,j=0,進入循環esle部分,j=nextval[j]=nextval[0]=-1;
- 進入循環的if部分,++i,++j,i=2,j=0,因爲ptrn[i]=ptrn[j]=a,所以nextval[2]=nextval[0]=-1;
- i=2, j=0, 由於ptrn[i]=ptrn[j],再次進入循環if部分,所以++i=3,++j=1,因爲ptrn[i]=ptrn[j]=b,所以nextval[3]=nextval[1]=0;
- i=3,j=1,由於ptrn[i]=ptrn[j]=b,所以++i=4,++j=2,因爲ptrn[i]!=ptrn[j],所以nextval[4]=2。
這樣上例中模式串的next數組各值最終應該爲:
圖4-1 正確的next數組各值
next數組求解的具體過程如下:
初始化:nextval[0] = -1,我們得到第一個next值即-1.
圖4-2 第一個next值即-1
i = 0,j = -1,由於j == -1,進入上述循環的if部分,++i得i=1,++j得j=0,且ptrn[i] != ptrn[j](即a!=b)),所以得到第二個next值即nextval[1] = 0;
圖4-3 第二個next值0
上面我們已經得到,i= 1,j = 0,由於不滿足條件j == -1 || ptrn[i] == ptrn[j],所以進入循環的esle部分,得j = nextval[j] = -1;此時,仍滿足循環條件,由於i = 1,j = -1,因爲j == -1,再次進入循環的if部分,++i得i=2,++j得j=0,由於ptrn[i] == ptrn[j](即ptrn[2]=ptrn[0],也就是說第1個元素和第三個元素都是a),所以進入循環if部分內嵌的else部分,得到nextval[2] = nextval[0] = -1;
圖4-4 第三個next數組元素值-1
i = 2,j = 0,由於ptrn[i] == ptrn[j],進入if部分,++i得i=3,++j得j=1,所以ptrn[i] == ptrn[j](ptrn[3]==ptrn[1],也就是說第2個元素和第4個元素都是b),所以進入循環if部分內嵌的else部分,得到nextval[3] = nextval[1] = 0;
圖4-5 第四個數組元素值0
如果你還是沒有弄懂上述過程是怎麼一回事,請現在拿出一張紙和一支筆出來,一步一步的畫下上述過程。相信我,把圖畫出來了之後,你一定能明白它的。
然後,我留一個問題給讀者,爲什麼上述的next數組要那麼求?有什麼原理麼?
- 5、利用求得的next數組各值運用Kmp算法
Ok,next數組各值已經求得,萬事俱備,東風也不欠了。接下來,咱們就要應用求得的next值,應用KMP算法來匹配字符串了。還記得KMP算法是怎麼一回事嗎?容我再次引用下之前的KMP算法的代碼,如下:
- //代碼5-1
- //int kmp_seach(char const*, int, char const*, int, int const*, int pos) KMP模式匹配函數
- //輸入:src, slen主串
- //輸入:patn, plen模式串
- //輸入:nextval KMP算法中的next函數值數組
- int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)
- {
- int i = pos;
- int j = 0;
- while ( i < slen && j < plen )
- {
- if( j == -1 || src[i] == patn[j] )
- {
- ++i;
- ++j;
- }
- else
- {
- j = nextval[j];
- //當匹配失敗的時候直接用p[j_next]與s[i]比較,
- //下面闡述怎麼求這個值,即匹配失效後下一次匹配的位置
- }
- }
- if( j >= plen )
- return i-plen;
- else
- return -1;
- }
圖5-1 求得的正確的next數組元素各值
以下是匹配過程,分三步:
第一步:主串和模式串如下,S[3]與P[3]匹配失敗。
圖5-2 第一步,S[3]與P[3]匹配失敗
第二步:S[3]保持不變,P的下一個匹配位置是P[next[3]],而next[3]=0,所以P[next[3]]=P[0],即P[0]與S[3]匹配。在P[0]與S[3]處匹配失敗。
圖5-3 第二步,在P[0]與S[3]處匹配失敗
第三步:與上文中第3小節末的情況一致。由於上述第三步中,P[0]與S[3]還是不匹配。此時i=3,j=nextval[0]=-1,由於滿足條件j==-1,所以進入循環的if部分,++i=4,++j=0,即主串指針下移一個位置,從P[0]與S[4]處開始匹配。最後j==plen,跳出循環,輸出結果i-plen=4(即字串第一次出現的位置),匹配成功,算法結束。
圖5-4 第三步,匹配成功,算法結束
所以,綜上,總結上述三步爲:
- 開始匹配,直到P[3]!=S[3],匹配失敗;
- nextval[3]=0,所以P[0]繼續與S[3]匹配,再次匹配失敗;
- nextval[0]=-1,滿足循環if部分條件j==-1,所以,++i,++j,主串指針下移一個位置,從P[0]與S[4]處開始匹配,最後j==plen,跳出循環,輸出結果i-plen=4,算法結束。
與上文中第3小節的四步匹配相比,本節運用修正過後的next數組,去掉了第3小節的第2個多餘步驟的nextval[3]=1,所以P[1]繼續與S[3]匹配,匹配失敗(緣由何在?因爲與第3小節的next數組相比,此時的next數組中nextval[3]已等於0)。所以,才只需要三個匹配步驟了。
ok,KMP算法已宣告完結,希望已經了卻了心中的一塊結石。畢竟,這個KMP算法此前也困擾了我很長一段時間。