由KMP算法談到BM算法

由KMP算法談到BM算法

出處:http://blog.csdn.net/v_july_v/article/details/6545192


引言

    在此之前,說明下寫作本文的目的:1、之前承諾過,這篇文章六、教你從頭到尾徹底理解KMP算法、updated之後,KMP算法會寫一個續集;2、寫這個kMP算法的文章很多很多,但真正能把它寫明白的少之又少;3、這個KMP算法曾經困擾過我很長一段時間。我也必須讓讀者真真正正徹徹底底的理解它。希望,我能做到。

    ok,子串的定位操作通常稱做串的模式匹配,是各種串處理系統中最重要的操作之一.在很多應用中都會涉及子串的定位問題,如普通的字符串查找問題.如果我們把模式匹配的串看成一字節流的話,那應用空間一下子就廣闊了很多,HTTP協議裏就是字節流,有各種關鍵的字節流字段,HTTP數據進行解釋就需要用到模式匹配算法.

   本文是試圖清楚的講解模式匹配算法裏兩個最爲重要的算法:KMPBM算法,這兩個算法都較爲高效,特別是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-1  
  2. //int search(char const*, int, char const*, int)  
  3. //查找出模式串patn在主串src中第一次出現的位置  
  4. //plen爲模式串的長度  
  5. //返回patn在src中出現的位置,當src中並沒有patn時,返回-1  
  6. int search(char const* src, int slen, char const* patn, int plen)  
  7. {  
  8.     int i = 0, j = 0;  
  9.     while( i < slen && j < plen )  
  10.     {  
  11.         if( src[i] == patn[j] )  //如果相同,則兩者++,繼續比較  
  12.         {  
  13.             ++i;      
  14.             ++j;             
  15.         }  
  16.         else  
  17.         {  
  18.             //否則,指針回溯,重新開始匹配  
  19.             i = i - j + 1;  //退回到最開始時比較的位置  
  20.             j = 0;  
  21.         }  
  22.     }  
  23.     if( j >= plen )  
  24.         return i - plen;  //如果字符串相同的長度大於模式串的長度,則匹配成功  
  25.     else  
  26.         return -1;  
  27. }  
     該算法思維比較簡單(但也常被一些公司做爲面試題),很容易分析出本算法的時間複雜度爲O(pattern_length*target_length),我們主要是把時間浪費在什麼地方呢,相信,你已經看到上面的代碼註釋中有這麼一句話:“指針回溯,重新開始匹配”,這句話的意思就是好比我們乘坐一輛火車已經離站好遠了,後來火車司機突然對全部乘客說,你們搭錯了列車,要換一輛火車。也就是說在咱們的字符串匹配中,本來已經比較到前面的字符去了,現在又要回到原來的某一個位置重新開始一個個的比較。這就是問題的癥結所在。

    在繼續分析之前,咱們來思考這樣一個問題:爲什麼快排或者堆排序比直接的選擇排序快?直接的選擇排序,每次都是重複的比較數值的大小,每掃描一次,只得出一個最大(小值),再沒有其它的結果信息能給下一次掃描帶來便捷。我們看看快排,每掃一次,將數據按某一值分成了兩邊,至少有右邊的數據都大於左邊的數據,所以在比較的時候,下一次就不用比較了。再看看堆排序,建堆的過程也是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來行而已。如下描述爲:

  1. //代碼2-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.             //當在j處,P[j]與S[i]匹配失敗的時候直接用patn[nextval[j]]繼續與S[i]比較,  
  21.             //所以,Kmp算法的關鍵之處就在於怎麼求這個值拉,  
  22.             //即匹配失效後下一次匹配的位置。下面,具體闡述。  
  23.         }  
  24.     }  
  25.     if( j >= plen )  
  26.         return i-plen;  
  27.     else  
  28.         return -1;  
  29. }     

 

  • 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]):

  1. P[j] = p[k], 那麼next[j+1]=k+1,這個很容易理解。採用遞推的方式求出next[j+1]=k+1(代碼3-1的if部分)。
  2. P[j] != p[k],那麼next[j+1]=next[k]+1(代碼3-1的else部分)

    稍後,你將看到,由這個方法得出的next值還不是最優的,也就是說是不能允許P[j]=P[next[j]]出現的。ok,請跟着我們一步一步登上山頂,不要試圖一步登天,那是不可能的。由以上,可得如下代碼:

  1. //代碼3-1,稍後,你將由下文看到,此求next數組元素值的方法有錯誤  
  2. void get_next(char const* ptrn, int plen, int* nextval)  
  3. {  
  4.     int i = 0;   
  5.     nextval[i] = -1;  
  6.     int j = -1;  
  7.     while( i < plen-1 )  
  8.     {  
  9.         if( j == -1 || ptrn[i] == ptrn[j] )    //循環的if部分  
  10.         {  
  11.             ++i;  
  12.             ++j;  
  13.             nextval[i] = j;  
  14.         }  
  15.         else                         //循環的else部分  
  16.             j = nextval[j];             //遞推  
  17.     }  
  18. }  
next數組求值的驗證   

    上述求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

    所以,綜上,總結上述四步爲: 

  1. P[3]!=S[3],匹配失敗;
  2. nextval[3]=1,所以P[1]繼續與S[3]匹配,匹配失敗;
  3. nextval[1]=0,所以P[0]繼續與S[3]匹配,再次匹配失敗;
  4. 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數組索引下的具體各值,區別細微。至於文中說不允許P[j]=P[next[j] ]出現,是因爲已經有P[3]=b與S[i]匹配敗,而P[next[3]]=P[1]=b,若再拿P[1]去與S[i]匹配則必敗。

  • 4、求解next數組各值的方法修正

    那麼,上面求解next數組各值的問題到底出現在哪兒呢?我們怎麼才能擺脫掉這種情況呢?:即不能讓P[j]=P[next[j]]成立成立。不能再出現上面那樣的情況啊!即不能有這種情況出現:P[3]=b,而竟也有P[next[3]]=P[1]=b
    讓我們再次回顧一下之前求next數組的函數代碼:

  1. //引用之前上文第3小節中的有錯誤的求next的代碼3-1。  
  2. void get_next(char const* ptrn, int plen, int* nextval)  
  3. {  
  4.  int i = 0;   
  5.  nextval[i] = -1;  
  6.  int j = -1;  
  7.  while( i < plen-1 )  
  8.  {  
  9.   if( j == -1 || ptrn[i] == ptrn[j] )    //循環的if部分  
  10.   {  
  11.    ++i;  
  12.    ++j;  
  13.    nextval[i] = j;  //這裏有問題  
  14.   }  
  15.   else                                   //循環的else部分  
  16.    j = nextval[j];            //遞推  
  17.  }  
  18. }  

    由上面之前的代碼,我們看到,在求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函數代碼如下:

  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;
  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,因爲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算法的代碼,如下:

  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,算法結束。

    與上文中第3小節的四步匹配相比,本節運用修正過後的next數組,去掉了第3小節的第2個多餘步驟的nextval[3]=1,所以P[1]繼續與S[3]匹配,匹配失敗(緣由何在?因爲與第3小節的next數組相比,此時的next數組中nextval[3]已等於0)。所以,才只需要三個匹配步驟了。

    ok,KMP算法已宣告完結,希望已經了卻了心中的一塊結石。畢竟,這個KMP算法此前也困擾了我很長一段時間。耐心點,慢慢來,總會搞懂的。閒不多說,接下來,咱們開始介紹BM算法。

 

 

第二部分、BM算法

  • 1、簡單的後比對算法

    爲了更好的理解BM算法,我分三步引入BM算法。首先看看下面的一個字符串匹配算法,它與前面的回溯法差不多,看看差別在哪兒。

  1. /*! int search_reverse(char const*, int, char const*, int) 
  2. */bref 查找出模式串patn在主串src中第一次出現的位置  
  3. */return patn在src中出現的位置,當src中並沒有patn時,返回-1    
  4. */  
  5. int search_reverse(char const* src, int slen, char const* patn, int plen)  
  6. {  
  7.     int s_idx = plen, p_idx;      
  8.     if (plen == 0)        
  9.         return -1;  
  10.     while (s_idx <= slen)//計算字符串是否匹配到了盡頭       
  11.     {     
  12.         p_idx = plen;     
  13.         while (src[--s_idx] == patn[--p_idx])//開始匹配       
  14.         {         
  15.             //if (s_idx < 0)   
  16.                 //return -1;      
  17.             if (p_idx == 0)       
  18.             {         
  19.                 return s_idx;         
  20.             }     
  21.         }     
  22.         s_idx += (plen - p_idx)+1;    
  23.     }     
  24.     return -1         
  25. }  

    仔細分析上面的代碼,可以看出該算法的思路是從模式串的後面向前面匹配的,如果後面的幾個都不匹配了,就可以直接往前面跳了,直覺上這樣匹配更快些。是否真是如此呢?請先看下面的例子。

  

上面是詳細的算法流程,接下來我們就用上面的例子,來引出壞2、字符規則,3、最好後綴規則,最終引出4、BM算法。

 

  • 2、壞字符規則

    在上面的例子裏面,第一步的時候,S[3] = c != P[3],下一步應該當整個模式串移過S[3]即可,因爲S[3]已經不可能與P中的任何一個部分相匹配了。那是不是隻是對於P中不存在的字符就這樣直接跳過呢,如果P中存在的字符該怎麼定位呢?

    如模式串爲P=acab,基於壞字符規則匹配步驟分解圖如下:

 

       從上面的例子可以看出,我們需要建一張表,表示P中字符存在的情況,不存在,則s_idx直接加上plen跳過該字符,如果存在,則需要找到從後往前最近的一個字符對齊匹配,如上面的例子便已經說明了壞字符規則匹配方法.

    再看下面的例子:

       由此可見,第一個匹配失敗的時候S[i]=c,主串指針需要+2纔有可能在下一次匹配成功,同理第二次匹配失敗的時候,S[i]=a,主串指針需要+3直接跳過a才能下一次匹本成功。

對於S[i]字符,有256種可能,所以需要對於模式串建立一張長度爲256的壞字符表,其中當P中沒出現的字符,表值爲plen,如果出現了,則設置爲最近的一個對齊的值。具體算法比較簡單如下:

  1. /* 
  2.  
  3.   函數:void BuildBadCharacterShift(char *, int, int*) 
  4. 目的:根據好後綴規則做預處理,建立一張好後綴表 
  5. 參數:   
  6. pattern => 模式串P   
  7. plen => 模式串P長度      
  8. shift => 存放壞字符規則表,長度爲的int數組       
  9. 返回:void            
  10. */  
  11. void BuildBadCharacterShift(char const* pattern, int plen, int* shift)  
  12. {  
  13.     forint i = 0; i < 256; i++ )     
  14.         *(shift+i) = plen;  
  15.     while ( plen >0 )          
  16.     {     
  17.         *(shift+(unsigned char)*pattern++) = --plen;          
  18.     }     
  19. }  
     這個時候整個算法的匹配算法該是怎麼樣的呢,是將上面的search_reverse函數中的s_idx+=(plen-p_idx)+1改成s_idx+= shift[(unsigned char)patn[p_idx]] +1嗎? 不是的,代碼給出如下,具體原因讀者可自行分析。

  1. /*! int search_badcharacter(char const*, int, char const*, int) 
  2.  
  3.   */bref 查找出模式串patn在主串src中第一次出現的位置  
  4.     
  5.     */return patn在src中出現的位置,當src中並沒有patn時,返回-1  
  6.       
  7. */  
  8. int search_badcharacter(char const* src, int slen, char const* patn, int plen, int* shift)  
  9. {     
  10.     int s_idx = plen, p_idx;    
  11.     int skip_stride;      
  12.     if (plen == 0)        
  13.         return -1;  
  14.     while (s_idx <= slen)//計算字符串是否匹配到了盡頭   
  15.     {     
  16.         p_idx = plen;     
  17.         while (src[--s_idx] == patn[--p_idx])//開始匹配       
  18.         {     
  19.             //if (s_idx < 0)   
  20.             //Return -1;      
  21.             if (p_idx == 0)       
  22.             {             
  23.                 return s_idx;         
  24.             }     
  25.         }  
  26.         skip_stride =  shift[(unsigned char)src[s_idx]];  
  27.         s_idx += (skip_stride>plen-p_idx ? skip_stride: plen-p_idx)+1;     
  28.     }     
  29.     return -1;    
  30. }  

  • 3、最好後綴規則

    在講最好後綴規則之前,我們回顧一下本部分第1小節中所舉的一個簡單後比對算法的例子:

  

       上面倒數第二步匹配是沒必要的。爲什麼呢?在倒數第三步匹配過程中,已有最後兩個字符與模式串P中匹配,而模式串中有前兩個與後兩個字符相同的,所以可以直接在接下來將P中的前兩個與主串中匹配過的ab對齊,做爲下一次匹配的開始。

其實思路與本文第一部分講過的KMP算法差不多,也是利用主串與模式串已匹配成功的部分來找一個合適的位置方便下一次最有效的匹配。只是這裏是需要尋找一個位置,讓已匹配過的後綴與模式串中從後往前最近的一個相同的子串對齊。(理解這句話就理解了BM算法的原理)這裏就不做數學描述了。

ok,主體思想有了,怎麼具體點呢?下面,直接再給一個例子,說明這種匹配過程。看下圖吧。

由圖可以goodsuffixshift[5] = 5

下面看goodsuffixshift [3]的求解

求解最好後綴數組是BM算法之所以難的根本,所以建議多花時間理清思路。網上有很多方法,我也試過兩個,一經測試,很多都不算準確,最好後綴碼的求解不像KMP的“最好前綴數組”那樣可以用遞推的方式求解,而是有很多細節。

代碼如下: 

  1. /* 
  2.   函數:void BuildGoodSuffixShift(char *, int, int*)  
  3. 目的:根據最好後綴規則做預處理,建立一張好後綴表     
  4. 參數: 
  5. pattern => 模式串P 
  6. plen => 模式串P長度 
  7. shift => 存放最好後綴表數組 
  8. 返回:void 
  9. */  
  10. void  BuildGoodSuffixShift(char const* pattern, int plen, int* shift)  
  11. {     
  12.     shift[plen-1] = 1;            // 右移動一位    
  13.     char end_val = pattern[plen-1];  
  14.     char const* p_prev, const* p_next, const* p_temp;  
  15.     char const* p_now = pattern + plen - 2;            // 當前配匹不相符字符,求其對應的shift  
  16.     bool isgoodsuffixfind = false;                    // 指示是否找到了最好後綴子串,修正shift值  
  17.     forint i = plen -2; i >=0; --i, --p_now)         
  18.     {     
  19.         p_temp = pattern + plen -1;   
  20.         isgoodsuffixfind = false;  
  21.         while ( true )        
  22.         {         
  23.             while (p_temp >= pattern && *p_temp-- != end_val);          // 從p_temp從右往左尋找和end_val相同的字符子串    
  24.             p_prev = p_temp;       // 指向與end_val相同的字符的前一個     
  25.             p_next = pattern + plen -2;             // 指向end_val的前一個  
  26.             // 開始向前匹配有以下三種情況      
  27.             //第一:p_prev已經指向pattern的前方,即沒有找到可以滿足條件的最好後綴子串  
  28.             //第二:向前匹配最好後綴子串的時候,p_next開始的子串先到達目的地p_now,   
  29.             //需要判斷p_next與p_prev是否相等,如果相等,則繼續住前找最好後綴子串     
  30.             //第三:向前匹配最好後綴子串的時候,p_prev開始的子串先到達端點pattern, 這個可以算是最好的子串  
  31.                     
  32.             if( p_prev < pattern  && *(p_temp+1) != end_val )         // 沒有找到與end_val相同字符      
  33.                 break;  
  34.   
  35.             bool  match_flag = true;        //連續匹配失敗標誌    
  36.             while( p_prev >= pattern && p_next > p_now )        
  37.             {  
  38.                 if( *p_prev --!= *p_next-- )          
  39.                 {     
  40.                     match_flag = false;      //匹配失敗   
  41.                     break;    
  42.                 }  
  43.             }  
  44.           
  45.             if( !match_flag )  
  46.                 continue;          //繼續向前尋找最好後綴子串  
  47.             else      
  48.             {     
  49.                 //匹配沒有問題, 是邊界問題  
  50.                 if( p_prev < pattern || *p_prev != *p_next)    
  51.                 {  
  52.                     // 找到最好後綴子串  
  53.                     isgoodsuffixfind = true;  
  54.                     break;    
  55.                 }  
  56.                 // *p_prev == * p_next  則繼續向前找    
  57.             }     
  58.         }  
  59.         shift[i] = plen - i + p_next - p_prev;  
  60.         if( isgoodsuffixfind )  
  61.             shift[i]--;               // 如果找到最好後綴碼,則對齊,需減修正  
  62.     }  
  63. }  

    注:代碼裏求得到的goodsuffixshift值與上述圖解中有點不同,這也是我看網上代碼時做的一個小改進。請注意。另外,如上述代碼的註釋裏所述,開始向前匹配有以下三種情況

  • 第一p_prev已經指向pattern的前方,即沒有找到可以滿足條件的最好後綴子串
  • 第二向前匹配最好後綴子串的時候,p_next開始的子串先到達目的地p_now, 需要判斷p_nextp_prev是否相等,如果相等,則繼續住前找最好後綴子串
  • 第三向前匹配最好後綴子串的時候,p_prev開始的子串先到達端點pattern, 這個可以算是最好的子串。下面,咱們分析這個例子    

從圖中可以看出,在模式串P中,P[2]=P[6]P[1]也等於P[5],所以如果只移5位讓P[2]S[6]對齊是沒必要的,因爲P[1]不可能與S[5]相等(如紅體字符表示),對於這種情況,P[2]=P[6]就不算最好後綴碼了,所以應該直接將整個P滑過S[6],所以goodsuffixshift[5]=8而不是5也就是說,在匹配過程中已經得出P[1]是不可能等於S[5]的,所以,就算爲了達到P[2]S[6]匹配的效果,讓模式串P右移5位,但在P[1]處與S[5]處還是會導致匹配失敗。所以,必定會匹配失敗的事,我們又何必多次一舉呢?

 

那麼,我們到底該怎麼做呢?如果我現在直接給出代碼的話,可能比較難懂,爲了進一步說明,以下圖解是將BM算法的好後綴表數組shift(不匹配時直接跳轉長度數組)的求解過程。其中第一行爲src數組,第二行爲patn數組,第三行爲匹配失敗時下一次匹配時的patn數組(粉色框的元素實際不存在)。

1i = 5時不匹配的情況

ok,現在咱們定位於P[5]處,當i = 5時src[5] != patn[5]p_now指向patn[5],而p_prev指向patn[1],即情況二。由於此時*p_prev == *p_now則繼續前找最好後綴子串。循環直到p_prev指向patn[0]的前一個位置(實際不存在,爲了好理解加上去的)。此時p_prev指向patn[0]的前方,即情況一。此時條件p_prev < pattern  && *(p_temp+1) != end_val滿足,所以跳出循環。計算shift[5]= plen - i + p_next - p_prev =8(實際上是第三行的長度)。

2、i = 4時不匹配的情況

i= 4時,src[4] != patn[4],此時p_prev指向patn[0]p_now指向patn[4],即情況二。由於此時*p_prev == *p_now則繼續前找最好後綴子串。循環直到p_prev指向patn[0]的前一個位置。此時p_prev指向patn[0]的前方,即情況一。此時條件p_prev < pattern  && *(p_temp+1) != end_val滿足,所以跳出循環。計算shift[4]= plen - i + p_next - p_prev =9(實際上是第三行的長度)。

3、i = 3時不匹配的情況

同樣的過程可以得到,i = 3shift[3]也爲第三行的長度7

4、i = 2時不匹配的情況

同樣的過程可以得到,i = 2shift[2]也爲第三行的長度8

5、i = 1時不匹配的情況

同樣的過程可以得到,i = 1shift[1]也爲第三行的長度9

6、i = 0時不匹配的情況

同樣的過程可以得到,i = 0shift[0]也爲第三行的長度10

計算好後綴表數組後,這種情況下的字模式匹配算法爲:

  1. /*! int search_goodsuffix(char const*, int, char const*, int) 
  2.  
  3.   */bref 查找出模式串patn在主串src中第一次出現的位置  
  4.     
  5.     */return patn在src中出現的位置,當src中並沒有patn時,返回-1  
  6.       
  7. */  
  8. int search_goodsuffix(char const* src, int slen, char const* patn, int plen, int* shift)  
  9. {  
  10.     int s_idx = plen, p_idx;    
  11.     int skip_stride;  
  12.     if (plen == 0)    
  13.         return -1;  
  14.       
  15.     while (s_idx <= slen)//計算字符串是否匹配到了盡頭   
  16.     {     
  17.         p_idx = plen;  
  18.         while (src[--s_idx] == patn[--p_idx])//開始匹配   
  19.         {     
  20.             //if (s_idx < 0)   
  21.                 //return -1;  
  22.             if (p_idx == 0)   
  23.             {         
  24.                 return s_idx;     
  25.             }  
  26.         }  
  27.         skip_stride =  shift[p_idx];  
  28.         s_idx += skip_stride +1;  
  29.     }  
  30.     return -1;  
  31. }  

  • 4、BM算法

    有了前面的三個步驟的算法的基礎,BM算法就比較容易理解了,其實BM算法就是將壞字符規則與最好後綴規則的綜合具體代碼如下,相信一看就會明白。

  1. /* 
  2. 函數:int* BMSearch(char *, int , char *, int, int *, int *) 
  3. 目的:判斷文本串T中是否包含模式串P 
  4. 參數: 
  5. src => 文本串T 
  6. slen => 文本串T長度 
  7. ptrn => 模式串P 
  8. pLen => 模式串P長度 
  9. bad_shift => 壞字符表 
  10. good_shift => 最好後綴表 
  11. 返回: 
  12. int - 1表示匹配失敗,否則反回                     
  13. */  
  14. int BMSearch(char const*src, int slen, char const*ptrn, int plen, int const*bad_shift, int const*good_shift)  
  15. {  
  16.     int s_idx = plen;    
  17.     if (plen == 0)    
  18.         return 1;  
  19.       
  20.     while (s_idx <= slen)//計算字符串是否匹配到了盡頭   
  21.     {     
  22.         int p_idx = plen, bad_stride, good_stride;    
  23.         while (src[--s_idx] == ptrn[--p_idx])//開始匹配       
  24.         {         
  25.             //if (s_idx < 0)   
  26.                 //return -1;  
  27.               
  28.             if (p_idx == 0)       
  29.             {         
  30.                 return s_idx;     
  31.             }     
  32.         }  
  33.           
  34.         // 當匹配失敗的時候,向前滑動  
  35.         bad_stride = bad_shift[(unsigned char)src[s_idx]];      //根據壞字符規則計算跳躍的距離  
  36.         good_stride = good_shift[p_idx];                                 //根據好後綴規則計算跳躍的距離  
  37.         s_idx += ((good_stride > bad_stride) ? good_stride : bad_stride )+1;//取大者      
  38.     }  
  39.     return -1;    
  40. }  



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