KMP算法之總結篇

KMP算法之總結篇

出處:http://blog.csdn.net/v_JULY_v/

引記

    此前一天,一位MS的朋友邀我一起去與他討論快速排序,紅黑樹,字典樹,B樹、後綴樹,包括KMP算法,唯獨在講解KMP算法的時候,言語磕磕碰碰,我想,原因有二:1、博客內的東西不常回顧,忘了不少;2、便是我對KMP算法的理解還不夠徹底,自不用說講解自如,運用自如了。所以,特再寫本篇文章。由於此前,個人已經寫過關於KMP算法的兩篇文章,所以,本文名爲:KMP算法之總結篇。

   本文分爲如下六個部分:

  1. 第一部分、再次回顧普通的BF算法與KMP算法各自的時間複雜度,並兩相對照各自的匹配原理;
  2. 第二部分、通過我此前第二篇文章的引用,用圖從頭到尾詳細闡述KMP算法中的next數組求法,並運用求得的next數組寫出KMP算法的源碼;
  3. 第三部分、KMP算法的兩種實現,代碼實現一是根據本人關於KMP算法的第二篇文章所寫,代碼實現二是根據本人的關於KMP算法的第一篇文章所寫;
  4. 第四部分、測試,分別對第三部分的兩種實現中next數組的求法進行測試,挖掘其區別之所在;
  5. 第五部分、KMP完整準確源碼,給出KMP算法的準確的完整源碼;
  6. 第六部分、一眼看出字符串的next數組各值,通過幾個例子,讓讀者能根據字符串本身一眼判斷出其next數組各值。

    力求讓此文徹底讓讀者洞穿此KMP算法,所有原理,來龍去脈,讓讀者搞個通通透透(注意,本文中第二部分及第三部分的代碼實現一的字符串下標i 從0開始計算,其它部分如第三部分的代碼實現二,第五部分,和第六部分的字符串下標i 皆是從1開始的)。

    在看本文之前,你心中如若對前綴和後綴這個兩個概念有自己的理解,便最好了。有些東西比如此KMP算法需要我們反覆思考,反覆求解才行。個人寫的關於KMP算法的第二篇文章爲:六(續)、從KMP算法一步一步談到BM算法;第一篇爲:六、教你初步瞭解KMP算法、updated(文末鏈接)。ok,若有任何問題,懇請不吝指正。多謝。

第一部分、KMP算法初解

1普通字符串匹配BF算法與KMP算法的時間複雜度比較

    KMP算法是一種線性時間複雜的字符串匹配算法,它是對BF算法(Brute-Force,最基本的字符串匹配算法的)改進。對於給的原始串S和模式串P,需要從字符串S中找到字符串P出現的位置的索引。

BF算法的時間複雜度O(strlen(S) * strlen(T)),空間複雜度O(1)

KMP算法的時間複雜度O(strlen(S) + strlen(T)),空間複雜度O(strlen(T))

2BF算法與KMP算法的區別

    假設現在S串匹配到i位置,T串匹配到j位置。那麼總的來說,兩種算法的主要區別在於失配的情況下,對[j]的值做的處理

   BF算法中,如果當前字符匹配成功,即s[i+j] == T[j],令j++,繼續匹配下一個字符;如果失配,即S[i + j] != T[j]需要讓i++,並且j= 0,即每次匹配失敗的情況下,模式串T相對於原始串S向右移動了一位。

    而KMP算法中,如果當前字符匹配成功,即S[i]==T[j],令i++j++,繼續匹配下一個字符;如果匹配失敗,即S[i] != T[j],需要保持i不變,並且讓j = next[j],這裏next[j] <=j -1,即模式串T相對於原始串S向右移動了至少1(移動的實際位數j - next[j]  >=1),

    如果下次匹配是基於T向右移動一位,那麼i之前的部分(即S[i-j+1 ~ i-1]),和j=next[j]之前的部分(即T[0 ~ j-2])仍然相等。顯然,相對於BF算法來說,KMP移動更多的位數,起到了一個加速的作用! (失配的特殊情形,令j=next[j]導致j==0的時候,需要將i ++,否則此時沒有移動模式串)

3、BF算法爲什麼要回溯

首先說一下爲什麼BF算法要回溯。如下兩字符串匹配(恰如上面所述:BF算法中,如果當前字符匹配成功,即s[i+j] == T[j],令j++,繼續匹配下一個字符):

      i+jjT中的j++變,而動)

S:aaaacefghij

         j++

T:aaac 

如果不回溯的話就是從下一位開始比起:

aaaacefghij

        aaac

看到上面紅顏色的沒,如果不回溯的話,那麼從a 的下一位比起。然而下述這種情況就漏了(正確的做法當然是要回溯:如果失配,即S[i + j] != T[j]需要讓i++,並且j= 0):

aaaacefghij

  aaac

    所以,BF算法要回溯,其代碼如下:

  1. int Index(SString S, SString T, int pos) {  
  2.    //返回T在S中第pos個字符之後的位置  
  3.    i=pos; j=1;k=0;  
  4.   while ( i< = S[0] && j< = T[0] ) {  
  5.       if (S[i+k] = = T[j] ) {++k;  ++j;}   //繼續比較後續字符  
  6.       else {i=i+1;   j=1; k=0;}      //指針回溯到 下一首位,重新開始  
  7.   }  
  8.   if(j>T[0]) return i;          //子串結束,說明匹配成功  
  9.   else return  0;  
  10. }//Index  

  不過,也有特殊情況可以不回溯,如下:
abcdefghij(主串)
abcdefg(模式串)
  即(模式串)沒有相同的纔不需要回溯。


4KMP 算法思想
    普通的字符串匹配算法必須要回溯。但回溯就影響了效率,回溯是由T串本身的性質決定的,是因爲T串本身有前後'部分匹配'的性質。像上面所說如果主串爲abcdef這樣的,大沒有回溯的必要。

    改進的地方也就是這裏,我們從T串本身出發,事先就找準了T自身前後部分匹配的位置,那就可以改進算法。

    如果不用回溯,那模式串下一個位置從哪裏開始呢?

    還是上面那個例子,T(模式串)ababc,如果c失配,那就可以往前移到aba最後一個a的位置,像這樣:

...ababd...

   ababc

    ->ababc

這樣i不用回溯,j跳到前2個位置,繼續匹配的過程,這就是KMP算法所在。這個當T[j]失配後,應該往前跳的值就是jnext,它是由T串本身固有決定的,與S(主串)無關


5、next數組的含義

重點來了。下面解釋一下next數組的含義,這個也是KMP算法中比較不好理解的一點。

  令原始串爲: S[i],其中0<=i<=n;模式串爲: T[j],其中0<=j<=m

  假設目前匹配到如下位置

               S0,S1,S2,...,Si-j,Si-j+1...............,Si-1Si, Si+1,....,Sn

                                   T0,T1,.....................,Tj-1Tj, ..........

  ST的綠色部分匹配成功,恰好到SiTj的時候失配,如果要保持i不變,同時達到讓模式串T相對於原始串S右移的話,可以更新j的值,讓Si和新的Tj進行匹配,假設新的jnext[j]表示,即讓Sinext[j]匹配,顯然新的j值要小於之前的j值,模式串纔會是右移的效果,也就是說應該有next[j] <= j -1。那新的j值也就是next[j]應該是多少呢?我們觀察如下的匹配:

      1)如果模式串右移1位(從簡單的思考起,移動一位會怎麼樣),即next[j] = j - 1, 即讓藍色的SiTj-1匹配(注:省略號爲未匹配部分)

               S0,S1,S2,...,Si-j,Si-j+1...............,Si-1Si, Si+1,....,Sn

                                   T0,T1,.....................,Tj-1Tj, .......... (T的劃線部分和S劃線部分相等【1】)

                                        T0,T1,.................Tj-2,Tj-1, ....... (移動後的T的劃線部分和S的劃線部分相等【2】)

        根據【1】【2】可以知道當next[j] =j -1,即模式串右移一位的時候,有T[0 ~ j-2] == T[1 ~ j-1],而這兩部分恰好是字符串T[0 ~j-1]的前綴和後綴,也就是說next[j]的值取決於模式串Tj前面部分的前綴和後綴相等部分的長度(好好揣摩這兩個關鍵字概念:前綴、後綴,或者再想想,我的上一篇文章,從Trie樹談到後綴樹中,後綴樹的概念)。

      2)如果模式串右移2位,即next[j] = j - 2, 即讓藍色的SiTj-2匹配    

               S0,S1,...,Si-j,Si-j+1,Si-j+2...............,Si-1Si, Si+1,....,Sn

                                   T0,T1,T2,.....................,Tj-1Tj, ..........(T的劃線部分和S劃線部分相等【3】)

                                              T0,T1,...............,Tj-3,Tj-2,.........(移動後的T的劃線部分和S的劃線部分相等【4】)

        同樣根據【3】【4】可以知道當next[j] =j -2,即模式串右移兩位的時候,有T[0 ~ j-3] == T[2 ~ j-1]。而這兩部分也恰好是字符串T[0 ~j-1]的前綴和後綴,也就是說next[j]的值取決於模式串Tj前面部分的前綴和後綴相等部分的長度

     3)依次類推,可以得到如下結論:當發生失配的情況下,j的新值next[j]取決於模式串中T[0 ~ j-1]中前綴和後綴相等部分的長度, 並且next[j]恰好等於這個最大長度

    爲此,請再允許我引用上文中的一段原文:KMP算法中,如果當前字符匹配成功,即S[i]==T[j],令i++j++,繼續匹配下一個字符;如果匹配失敗,即S[i] != T[j],需要保持i不變,並且讓j = next[j],這裏next[j] <=j -1,即模式串T相對於原始串S向右移動了至少1(移動的實際位數j - next[j]  >=1),

    同時移動之後,i之前的部分(即S[i-j+1 ~ i-1]),和j=next[j]之前的部分(即T[0 ~ j-2])仍然相等。顯然,相對於BF算法來說,KMP移動更多的位數,起到了一個加速的作用! (失配的特殊情形,令j=next[j]導致j==0的時候,需要將i ++,否則此時沒有移動模式串)。”

    於此,也就不難理解了我的關於KMP算法的第二篇文章之中:當匹配到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的最大值。”。

      根據上文的【1】與【2】的匹配情況,可得第二篇文章之中所謂的k=1(如aaaa的形式),根據上文的【3】與【4】的匹配情況,k=2(如abab的形式)。

      再次總結下,如下圖:

    從上圖中我們看到,當S移動到i,P到j的時候失配。這時候i不回朔,而只是將P向前移動儘可能的距離,繼續比較。
    假設,P向右移動一定距離後,第k個字符P[k]和S[i]進行比較。此時如上圖,當P[j]和S[i]失配後,i不動,將P前移到K,讓P[k]和S[i]繼續匹配。現在的關鍵是K的值是多少?
    通過上圖,我們發現,因爲黃色部分表示已經匹配了的結果(因爲是到了S[i]和P[j]的時候才失配,所以Si-j+1Si-j+2…Si-1= P1P2…Pj-1,見黃色的部分)。所以有:
1、 Si-k+1Si-k+2…Si-1 = Pj-k+1Pj-k+2…Pj-1
所以當P前移到K時,有:
2、 Si-k+1Si-k+2…Si-1 = P1P2…Pk-1
通過1,2=>
Pj-k+1Pj-k+2…Pj-1 = P1P2…Pk-1
而P1P2…Pk-1和Pj-k+1Pj-k+2…Pj-1就相當於P串的前綴和後綴,前已說過,你心中一定要有前綴和後綴的概念或意識。

     所以,歸根究底,KMP算法的本質便是:每一次匹配都是基於前一次匹配的結果,如何更好地利用這前一次匹配的結果呢?針對待匹配的模式串的特點,判斷它是否有重複的字符,從而找到它的前綴與後綴,進而求出相應的Next數組,最終根據Next數組而進行KMP匹配。接下來,進入本文的第二部分。

第二部分、next數組求法的來龍去脈與KMP算法的源碼

    本部分引自個人此前的關於KMP算法的第二篇文章:六之續、由KMP算法談到BM算法。前面,我們已經知道即不能讓P[j]=P[next[j]]成立成立。不能再出現上面那樣的情況啊!即不能有這種情況出現:P[3]=b,而竟也有P[next[3]]=P[1]=b

    正如在第二篇文章中,所提到的那樣:“這裏讀者理解可能有困難的是因爲文中,時而next,時而nextval,把他們的思維搞混亂了。其實next用於表達數組索引,而nextval專用於表達next數組索引下的具體各值,區別細微。至於文中說不允許P[j]=P[next[j] ]出現,是因爲已經有P[3]=b與S[i]匹配敗,而P[next[3]]=P1=b,若再拿P[1]=b去與S[i]匹配則必敗。”--六之續、由KMP算法談到BM算法。

   又恰恰如上文中所述:“模式串T相對於原始串S向右移動了至少1(移動的實際位數j - next[j]  >=1)

    ok,求next數組的get_nextval函數正確代碼如下:

  1. //代碼4-1    
  2. //修正後的求next數組各值的函數代碼    
  3. void get_nextval(char const* ptrn, int plen, int* nextval)    
  4. {    
  5.     int i = 0;     
  6.     nextval[i] = -1;    
  7.     int j = -1;    
  8.     while( i < plen-1 )    
  9.     {    
  10.         if( j == -1 || ptrn[i] == ptrn[j] )   //循環的if部分    
  11.         {    
  12.             ++i;    
  13.             ++j;    
  14.             //修正的地方就發生下面這4行    
  15.             if( ptrn[i] != ptrn[j] ) //++i,++j之後,再次判斷ptrn[i]與ptrn[j]的關係    
  16.                 nextval[i] = j;      //之前的錯誤解法就在於整個判斷只有這一句。    
  17.             else    
  18.                 nextval[i] = nextval[j];    
  19.         }    
  20.         else                                 //循環的else部分    
  21.             j = nextval[j];    
  22.     }    
  23. }    

    舉個例子,舉例說明下上述求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數組各值怎麼求出來的呢?分以下五步:

  1. 初始化:i=0,j=-1,nextval[0] = -1由於j == -1,進入上述循環的if部分,++i得i=1,++j得j=0,且ptrn[i] != ptrn[j](即a!=b)),所以得到第二個next值即nextval[1] = 0;
  2. i=1,j=0,進入循環esle部分,j=nextval[j]=nextval[0]=-1;
  3. 進入循環的if部分,++i,++j,i=2,j=0,因爲ptrn[i]=ptrn[j]=a,所以nextval[2]=nextval[0]=-1;
  4. i=2, j=0, 由於ptrn[i]=ptrn[j],再次進入循環if部分,所以++i=3,++j=1,因爲ptrn[i]=ptrn[j]=b,所以nextval[3]=nextval[1]=0;
  5. i=3,j=1,由於ptrn[i]=ptrn[j]=b,所以++i=4,++j=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數組要那麼求?有什麼原理麼?

    提示:我們從上述字符串abab 各字符的next值-1 0 -1 0,可以看出來,根據求得的next數組值,偷用前綴、後綴的概念,一定可以判斷出在abab之中,前綴和後綴相同,即都是ab,反過來,如果一個字符串的前綴和後綴相同,那麼根據前綴和後綴依次求得的next各值也是相同的。

  • 5、利用求得的next數組各值運用Kmp算法

    Ok,next數組各值已經求得,萬事俱備,東風也不欠了。接下來,咱們就要應用求得的next值,應用KMP算法來匹配字符串了。還記得KMP算法是怎麼一回事嗎?容我再次引用下之前的KMP算法的代碼,如下:

  1. //代碼5-1    
  2. //int kmp_seach(char const*, int, char const*, int, int const*, int pos)  KMP模式匹配函數    
  3. //輸入:src, slen主串    
  4. //輸入:patn, plen模式串    
  5. //輸入:nextval KMP算法中的next函數值數組    
  6. int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)    
  7. {    
  8.     int i = pos;    
  9.     int j = 0;    
  10.     while ( i < slen && j < plen )    
  11.     {    
  12.         if( j == -1 || src[i] == patn[j] )    
  13.         {    
  14.             ++i;    
  15.             ++j;    
  16.         }    
  17.         else    
  18.         {    
  19.             j = nextval[j];              
  20.             //當匹配失敗的時候直接用p[j_next]與s[i]比較,    
  21.             //下面闡述怎麼求這個值,即匹配失效後下一次匹配的位置    
  22.         }    
  23.     }    
  24.     if( j >= plen )    
  25.         return i-plen;    
  26.     else    
  27.         return -1;    
  28. }    

我們上面已經求得的next值,如下:

        圖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 第三步,匹配成功,算法結束
    所以,綜上,總結上述三步爲

  1. 開始匹配,直到P[3]!=S[3],匹配失敗;
  2. nextval[3]=0,所以P[0]繼續與S[3]匹配,再次匹配失敗;
  3. nextval[0]=-1,滿足循環if部分條件j==-1,所以,++i,++j,主串指針下移一個位置,從P[0]與S[4]處開始匹配,最後j==plen,跳出循環,輸出結果i-plen=4,算法結束。

第三部分、KMP算法的兩種實現

代碼實現一:   

    根據上文中第二部分內容的解析,完整寫出KMP算法的代碼已經不是難事了,如下:

  1. //copyright@2011 binghu and july  
  2. #include "StdAfx.h"  
  3. #include <string>  
  4. #include <iostream>  
  5. using namespace std;  
  6.   
  7. //代碼4-1    
  8. //修正後的求next數組各值的函數代碼    
  9. void get_nextval(char const* ptrn, int plen, int* nextval)    
  10. {    
  11.     int i = 0;  //注,此處與下文的代碼實現二不同的是,i是從0開始的(代碼實現二i從1開始)     
  12.     nextval[i] = -1;    
  13.     int j = -1;    
  14.     while( i < plen-1 )    
  15.     {    
  16.         if( j == -1 || ptrn[i] == ptrn[j] )   //循環的if部分    
  17.         {    
  18.             ++i;    
  19.             ++j;    
  20.             //修正的地方就發生下面這4行    
  21.             if( ptrn[i] != ptrn[j] ) //++i,++j之後,再次判斷ptrn[i]與ptrn[j]的關係    
  22.                 nextval[i] = j;      //之前的錯誤解法就在於整個判斷只有這一句。    
  23.             else    
  24.                 nextval[i] = nextval[j];    
  25.         }    
  26.         else                                 //循環的else部分    
  27.             j = nextval[j];    
  28.     }    
  29. }    
  30.   
  31. void print_progress(char const* src, int src_index, char const* pstr, int pstr_index)  
  32. {  
  33.     cout<<src_index<<"\t"<<src<<endl;  
  34.     cout<<pstr_index<<"\t";  
  35.     forint i = 0; i < src_index-pstr_index; ++i )  
  36.         cout<<" ";  
  37.     cout<<pstr<<endl;  
  38.     cout<<endl;  
  39. }  
  40.   
  41. //代碼5-1    
  42. //int kmp_seach(char const*, int, char const*, int, int const*, int pos)  KMP模式匹配函數    
  43. //輸入:src, slen主串    
  44. //輸入:patn, plen模式串    
  45. //輸入:nextval KMP算法中的next函數值數組    
  46. int kmp_search(char const* src, int slen, char const* patn, int plen, int const* nextval, int pos)    
  47. {    
  48.     int i = pos;    
  49.     int j = 0;    
  50.     while ( i < slen && j < plen )    
  51.     {    
  52.         if( j == -1 || src[i] == patn[j] )    
  53.         {    
  54.             ++i;    
  55.             ++j;    
  56.         }    
  57.         else    
  58.         {    
  59.             j = nextval[j];              
  60.             //當匹配失敗的時候直接用p[j_next]與s[i]比較,    
  61.             //下面闡述怎麼求這個值,即匹配失效後下一次匹配的位置    
  62.         }    
  63.     }    
  64.     if( j >= plen )    
  65.         return i-plen;    
  66.     else    
  67.         return -1;    
  68. }    
  69.   
  70. int   main()  
  71. {  
  72.     std::string src = "aabcabcebafabcabceabcaefabcacdabcab";  
  73.     std::string prn = "abac";  
  74.   
  75.     int* nextval = new int[prn.size()];  
  76.     //int* next = new int[prn.size()];  
  77.     get_nextval(prn.data(), prn.size(), nextval);  
  78.     //get_next(prn.data(), prn.size(), next);  
  79.   
  80.     forint i = 0; i < prn.size(); ++i )  
  81.         cout<<nextval[i]<<"\t";  
  82.     cout<<endl;  
  83.       
  84.     cout<<"result sub str: "<<src.substr( kmp_search(src.data(), src.size(), prn.data(), prn.size(), nextval, 0) )<<endl;  
  85.     system("pause");  
  86.   
  87.     delete[] nextval;  
  88.     return 0;  
  89. }   

    運行結果,如下圖所示:

代碼實現二

     再給出代碼實現二之前,讓我們再次回顧下關於KMP算法的第一篇文章中的部分內容

第二節、KMP算法

2.1、 覆蓋函數(overlay_function)

    覆蓋函數所表徵的是pattern本身的性質,可以讓爲其表徵的是pattern從左開始的所有連續子串的自我覆蓋程度。比如如下的字串,abaabcaba

    可能上面的圖令讀者理解起來還是不那麼清晰易懂,其實很簡單,針對字符串abaabcaba

a(-1) b(-1)a(0) a0 b(1) c(-1) a(0) b(1)a(2)

解釋:

  1. 初始化爲-1  
  2. b與a不同爲-1   
  3. 與第一個字符a相同爲0   
  4. 還是a爲0   
  5. 後綴ab與前綴ab兩個字符相同爲1 
  6. 前面並無前綴c爲-1  
  7. 與第一個字符同爲0  
  8. 後綴ab前綴ab爲1 
  9. 前綴aba後綴aba爲2

    由於計數是從0始的,因此覆蓋函數的值爲0說明有1個匹配,對於從0還是從來開始計數是偏好問題,具體請自行調整,其中-1表示沒有覆蓋,那麼何爲覆蓋呢,下面比較數學的來看一下定義,比如對於序列

  a0a1...aj-1 aj

要找到一個k,使它滿足

  a0a1...ak-1ak=aj-kaj-k+1...aj-1aj

    而沒有更大的k滿足這個條件,就是說要找到儘可能大k,使pattern前k字符與後k字符相匹配,k要儘可能的大,原因是如果有比較大的k存在。

    但若我們選擇較小的滿足條件的k,那麼當失配時,我們就會使pattern向右移動的位置變大,而較少的移動位置是存在匹配的,這樣我們就會把可能匹配的結果丟失。比如下面的序列,

    在紅色部分失配,正確的結果是k=1的情況,把pattern右移4位,如果選擇k=0,右移5位則會產生錯誤。計算這個overlay函數的方法可以採用遞推,可以想象如果對於pattern的前j個字符,如果覆蓋函數值爲k

    a0a1...ak-1ak=aj-kaj-k+1...aj-1aj
則對於pattern的前j+1序列字符,則有如下可能
    ⑴     pattern[k+1]==pattern[j+1] 此時overlay(j+1)=k+1=overlay(j)+1
    ⑵     pattern[k+1]≠pattern[j+1] 此時只能在pattern前k+1個子符組所的子串中找到相應的overlay函數,h=overlay(k),如果此時pattern[h+1]==pattern[j+1],則overlay(j+1)=h+1否則重複(2)過程.

下面給出一段計算覆蓋函數的代碼:

  1. //copyright@ staurman  
  2. //updated@2011 July  
  3. #include "StdAfx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. using namespace std;  
  7.   
  8. //solve to the next array  
  9. void compute_overlay(const string& pattern)  
  10. {  
  11.     const int pattern_length = pattern.size();  
  12.     int *overlay_function = new int[pattern_length];  
  13.     int index;  
  14.     overlay_function[0] = -1;  
  15.     for(int i=1;i<pattern_length;++i)      
  16.         //注,與上文代碼段一不同的是,此處i是從1開始的,所以,下文中運用倆種方法求出來的next數組各值會有所不同  
  17.     {  
  18.         index = overlay_function[i-1];  
  19.         //store previous fail position k to index;  
  20.   
  21.         while(index>=0 && pattern[i]!=pattern[index+1])  
  22.         {  
  23.             index = overlay_function[index];  
  24.         }  
  25.         if(pattern[i]==pattern[index+1])  
  26.         {  
  27.             overlay_function[i] = index + 1;    
  28.         }  
  29.         else  
  30.         {  
  31.             overlay_function[i] = -1;  
  32.         }  
  33.     }  
  34.     for(int i=0;i<pattern_length;++i)  
  35.     {  
  36.         cout<<overlay_function[i]<<endl;  
  37.     }  
  38.     delete[] overlay_function;  
  39. }  
  40.   
  41. //abaabcaba  
  42. int main()  
  43. {  
  44.     string pattern = "abaabcaba";  
  45.     compute_overlay(pattern);  
  46.     system("pause");  
  47.     return 0;  
  48. }  

    運行結果如下所示:

2.2、kmp算法
     有了覆蓋函數,那麼實現kmp算法就是很簡單的了,我們的原則還是從左向右匹配,但是當失配發生時,我們不用把target_index向回移動,target_index前面已經匹配過的部分在pattern自身就能體現出來,只要動pattern_index就可以了。

當發生在j長度失配時,只要把pattern向右移動j-overlay(j)長度就可以了。

     如果失配時pattern_index==0,相當於pattern第一個字符就不匹配,這時就應該把target_index加1,向右移動1位就可以了。

    ok,下圖就是KMP算法的過程(紅色即是採用KMP算法的執行過程):

    (另一作者saturnman發現,在上述KMP匹配過程圖中,index=8和index=11處畫錯了。還有,anaven也早已發現,index=3處也畫錯了。非常感謝。但圖已無法修改,見諒)

KMP 算法可在O(n+m)時間內完成全部的串的模式匹配工作。

    OK,下面此前寫的關於KMP算法的第一篇文章中的源碼:

  1. //copyright@ saturnman  
  2. //updated@ 2011 July  
  3. #include "stdafx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. #include <vector>  
  7. using namespace std;  
  8.   
  9. int kmp_find(const string& target,const string& pattern)  
  10. {  
  11.     const int target_length=target.size();  
  12.     const int pattern_length=pattern.size();  
  13.     int* overlay_value=new int[pattern_length];  
  14.     overlay_value[0]=-1;        //remember:next array's first number was -1.  
  15.     int index=0;  
  16.   
  17.     //next array  
  18.     for (int i=1;i<pattern_length;++i)  
  19.         //注,此處的i是從1開始的  
  20.     {  
  21.         index=overlay_value[i-1];  
  22.         while (index>=0 && pattern[index+1]!=pattern[i])  //remember:!=  
  23.         {  
  24.             index=overlay_value[index];  
  25.         }  
  26.         if(pattern[index+1] == pattern[i])  
  27.         {  
  28.             overlay_value[i]=index+1;  
  29.         }  
  30.         else  
  31.         {  
  32.             overlay_value[i]=-1;  
  33.         }  
  34.     }  
  35.   
  36.     //mach algorithm start  
  37.     int pattern_index=0;  
  38.     int target_index=0;  
  39.     while (pattern_index<pattern_length && target_index<target_length)  
  40.     {  
  41.         if (target[target_index] == pattern[pattern_index])  
  42.         {  
  43.             ++target_index;  
  44.             ++pattern_index;  
  45.         }   
  46.         else if(pattern_index==0)  
  47.         {  
  48.             ++target_index;  
  49.         }  
  50.         else  
  51.         {  
  52.             pattern_index=overlay_value[pattern_index-1]+1;  
  53.         }  
  54.     }  
  55.     if (pattern_index==pattern_length)  
  56.     {  
  57.         return target_index-pattern_index;  
  58.     }   
  59.     else  
  60.     {  
  61.         return -1;  
  62.     }  
  63.     delete [] overlay_value;  
  64. }  
  65.   
  66. int main()  
  67. {  
  68.     string sourc="ababc";  
  69.     string pattern="abc";  
  70.     cout<<kmp_find(sourc,pattern)<<endl;  
  71.     system("pause");  
  72.     return 0;  
  73. }  

    由於是abc跟ababc匹配,那麼將返回匹配的位置“2”,運行結果如所示:

第四部分、測試

    針對上文中第三部分的兩段代碼測試了下,糾結了,兩種求next數組的方法對同一個字符串求next數組各值,得到的結果竟然不一樣,如下二圖所示:

    1、兩種方法對字符串abab求next數組各值比較(下圖左邊爲代碼實現一內求next數組方法的結果,右邊爲代碼實現二內求next數組方法的結果):

    2、兩種對字符串abaabcaba求next數組各值比較(下圖左邊爲代碼實現一內求next數組方法的結果,右邊爲代碼實現二內求next數組方法的結果):

    爲何會這樣呢,其實很簡單,上文中已經有所說明了,代碼實現一的i 是從0開始的,代碼實現二的i 是從1開始的。但從最終的運行結果來看,暫時還是以代碼實現段二爲準。

第五部分、KMP完整準確源碼

    求next數組各值的方法爲:

  1. //copyright@ staurman  
  2. //updated@2011 July  
  3. #include "StdAfx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. using namespace std;  
  7.   
  8. //solve to the next array  
  9. void compute_overlay(const string& pattern)  
  10. {  
  11.     const int pattern_length = pattern.size();  
  12.     int *overlay_function = new int[pattern_length];  
  13.     int index;  
  14.     overlay_function[0] = -1;  
  15.     for(int i=1;i<pattern_length;++i)  
  16.     {  
  17.         index = overlay_function[i-1];  
  18.         //store previous fail position k to index;  
  19.   
  20.         while(index>=0 && pattern[i]!=pattern[index+1])  
  21.         {  
  22.             index = overlay_function[index];  
  23.         }  
  24.         if(pattern[i]==pattern[index+1])  
  25.         {  
  26.             overlay_function[i] = index + 1;    
  27.         }  
  28.         else  
  29.         {  
  30.             overlay_function[i] = -1;  
  31.         }  
  32.     }  
  33.     for(int i=0;i<pattern_length;++i)  
  34.     {  
  35.         cout<<overlay_function[i]<<endl;  
  36.     }  
  37.     delete[] overlay_function;  
  38. }  
  39.   
  40. //abaabcaba  
  41. int main()  
  42. {  
  43.     string pattern = "abaabcaba";  
  44.     compute_overlay(pattern);  
  45.     system("pause");  
  46.     return 0;  
  47. }  

    運行結果入下圖所示:abab的next數組各值是-1,-1,0,1,而非本文第二部分所述的-1,0,-1,0。爲什麼呢?難道是搬石頭砸了自己的腳?

    NO,上文第四部分末已經詳細說明,上處代碼i 從0開始,本文第二部分代碼i 從1開始。

    KMP算法完整源碼,如下:

  1. //copyright@ saturnman  
  2. //updated@ 2011 July  
  3. #include "stdafx.h"  
  4. #include<iostream>  
  5. #include<string>  
  6. #include <vector>  
  7. using namespace std;  
  8.   
  9. int kmp_find(const string& target,const string& pattern)  
  10. {  
  11.     const int target_length=target.size();  
  12.     const int pattern_length=pattern.size();  
  13.     int* overlay_value=new int[pattern_length];  
  14.     overlay_value[0]=-1;        //remember:next array's first number was -1.  
  15.     int index=0;  
  16.   
  17.     //next array  
  18.     for (int i=1;i<pattern_length;++i)  
  19.         //注,此處的i是從1開始的  
  20.     {  
  21.         index=overlay_value[i-1];  
  22.         while (index>=0 && pattern[index+1]!=pattern[i])    
  23.         {  
  24.             index=overlay_value[index];  
  25.         }  
  26.         if(pattern[index+1] == pattern[i])  
  27.         {  
  28.             overlay_value[i]=index+1;  
  29.         }  
  30.         else  
  31.         {  
  32.             overlay_value[i]=-1;  
  33.         }  
  34.     }  
  35.   
  36.     //mach algorithm start  
  37.     int pattern_index=0;  
  38.     int target_index=0;  
  39.     while (pattern_index<pattern_length && target_index<target_length)  
  40.     {  
  41.         if (target[target_index] == pattern[pattern_index])  
  42.         {  
  43.             ++target_index;  
  44.             ++pattern_index;  
  45.         }   
  46.         else if(pattern_index==0)  
  47.         {  
  48.             ++target_index;  
  49.         }  
  50.         else  
  51.         {  
  52.             pattern_index=overlay_value[pattern_index-1]+1;  
  53.         }  
  54.     }  
  55.     if (pattern_index==pattern_length)  
  56.     {  
  57.         return target_index-pattern_index;  
  58.     }   
  59.     else  
  60.     {  
  61.         return -1;  
  62.     }  
  63.     delete [] overlay_value;  
  64. }  
  65.   
  66. int main()  
  67. {  
  68.     string sourc="ababc";  
  69.     string pattern="abc";  
  70.     cout<<kmp_find(sourc,pattern)<<endl;  
  71.     system("pause");  
  72.     return 0;  
  73. }  

    運行結果如下:

第六部分、一眼看出字符串的next數組各值

    上文已經用程序求出了一個字符串的next數組各值,接下來,稍稍演示下,如何一眼大致判斷出next數組各值,以及初步判斷某個程序求出的next數組各值是不是正確的。有一點務必注意:下文中的代碼全部採取代碼實現二,即i是從1開始的

  • 1、對字符串aba求next數組各值,各位可以先猜猜,-1,...,aba中,a初始化爲-1,第二個字符b與a不同也爲-1,最後一個字符和第一個字符都是a,所以,我猜其next數組各值應該是-1,-1,0,結果也不出所料,如下圖所示:

  • 2、字符串“abab”呢,不用猜了,我已經看出來了,當然上文中代碼實現一和代碼實現二都已經求出來了。如果i 是1開始的話,那麼next數組各值將如代碼實現二所運行的那樣,將是:-1,-1,0,1;
  • 3、字符串“abaabcaba”呢,next數組如上第三部分代碼實現二所述,爲-1,-1,0,0,1,-1,0,1,2;
  • 4、字符串“abcdab”呢,next數組各值將是-1,-1,-1,-1,0,1;
  • 5、字符串“abcdabc”呢,next數組各值將是-1,-1,-1,-1,0,1,2;
  • 6、字符串“abcdabcd”呢,那麼next數組各值將是-1,-1,-1,-1,0,1,2,3;

    怎麼樣,看出規律來了沒?呵呵,可以用上述第五部分中求next數組的方法自個多試探幾次,相信,很快,你也會跟我一樣,不用計算,一眼便能看出某個字符串的next數組各值了。如此便恭喜你,理解了next數組的求法,KMP算法也就算是真真正正徹徹底底的理解了(至於如何運用求得的next數組各值來進行kmp算法的匹配的具體方法與過程,請轉到本文第二部分。不過,需要你注意的是,本文第二部分的i 是從0開始的)。完。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章