串之BF、KMP算法完美圖解


講這兩算法之前,我們首先了解幾個概念:

串:又稱字符串,由零個或多個字符組成的有限序列,如S="abcdef"。

子串:串中任意個連續的字符組成的子序列,稱為該串的子串,原串稱為子串的主串。如T="cde",T是S的子串。子串在主串中的位置,用子串的第一個字符在主串中出現的位置表示,T在S中的位置為3。

模式匹配:模式串的定位運算稱為串的模式匹配或串匹配。

假設有兩個串S,T,設S主串,也稱正文串,T為子串(S包含T),也稱模式串(與S進行匹配的串)。在匹配前,T稱為模式串更為合適,是因為T有可能不是S子序列

在主串S中查找與模式T相匹配的子串,如果查找成功,返回匹配的子串第一個字符在主串中的位置最笨的辦法就是窮舉所有S的所有子串,判斷是否與T匹配。

時刻注意不管是BF算法還是KMP算法,如果第一次比較就匹配了,程序自然就結束了。故關鍵是在出現不匹配時,如何確定下一輪進行比較即主串 i 的值Si與子串 j 的值Tj

例如:S="abaabaabeca",T=" abaabe",求子串T在主串S中的位置, 其中用 i,j 分別表示ST正在進行匹配字符的位置。 

1. S 串第1個字符開始: i=1, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;如果不等則執行第2步;

2. S 串第2個字符開始:即 i 退回到 i- j+2 (i-(j-1)+1) 的位置,其中 j-1 含義是不等之前比較的次數, 簡稱增量i-增量意味着 i 回到初值+1便是回溯到初值的下一位

i=2, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;如果不等則執行第3步;

3. S 串第3個字符開始:即 i 退回到 i- j+2的位置,即 i=3, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;如果不等則執行第4步;

 4. S 串第4個字符開始:即 i 退回到 i- j+2的位置,即 i=4, j=1,比較兩個字符是否相等,如果相等,則 i++, j++;由於此時 T 串比較完了,執行第5步;

 5. 需要返回子串T在主串S第一個字符出現的位置j( ) 移動了T.length次,即 i - T.length=10-6=4。因已匹配,不需要+1回溯到一開始位置的下個位置了。

其代碼如下:

int Index_BF(SString S, SString T) //返回模式串T在主串S中第一次出現的位置。因返回的是位置,不是狀態,雖都是int,但不用Status,另說清誰是模式串,誰是主串。

  { 
    int i=1,j=1,sum=0;   // 指示主串S中進行匹配字符的位置指示主串T中進行匹配字符的位置;sum用於累計比較次數;
    while (i<=S.length&&j<=T.length)//看上面例子分析得出,匹配只有成功和不成功兩種情形,換成計算機語言也就是if   else;

      {

          sum++;
        if (S.ch[i]==T.ch[j])  //看上面例子,不管那一輪只要成功,那么有i++ ; j++
          {
            i++;
            j++;          
          }
        else  //看上面例子,不管那一輪只要失敗,那么有i回溯到初值的下一位; j 始終是第1個和Si,即j=1;
          {
            i=i-j+2;   //i=i-(j-1)+1,看上面例子中的解釋;
            j=1;         
          }
      }//循環完也只有兩種情形,要么匹配,要么失敗,換成計算機語言仍就是if   else;

     cout<<"BF一共比較了"<<sum<<"次"<<endl;
    if(j>T.length)
      return (i-T.length); //返回位置. 比較成功的次數是T.length, i從初值也后移了T.length,i-T.length回到初值  
    else
        return 0;
  }

上述算法稱為 BF(Brute Force) 算法,Brute Force的意思是蠻力,暴力窮舉。其時間復雜度最壞達到O(n*m),n,m分別為S、T串的長度。

實際上,完全沒必要從S的每一個字符開始,暴力窮舉每一種情況,Knuth、Morris和Pratt對該算法進行了改進,稱為KMP算法

我們再回頭看剛才的例子:

S串第1個字符開始:i=1,j=1,比較兩個字符是否相等,如果相等,則i++,j++;按照BF算法,如果不等則i退回到i-j+2的位置,即i=2,j=1。

其實 i 不用回退 j 回退到第3個位置接着比較即可(KMP算法

 是不是像T串向右滑動了一段距離?為什么可以這樣?為什么讓 回退到第3個位置?而不是第2個,第4個?

因為T串中開頭的兩個字符  指向的字符前面的兩個字符一模一樣噢那 j 就可以回退到第3個位置繼續比較了因為前面兩個字符已經相等,不用比較了,如下圖

問題來了, 那我們怎么知道T串中開頭的兩個字符和 指向的字符前面的兩個字符一模一樣?難道還要比較?

分析發現: 指向的字符前面的兩個字符 和 T串中 指向字符前面的兩個字符一模一樣因為它們一直相等,才會i++,j++走到后面的位置,不僅主串字串這兩個字符一樣,ij之前所有字符也一樣(S1S2……Si-1=T1T2……Tj-1)。

     也就是說,我們不必判斷T串中開頭的兩個字母S串中 i 指向的字符前面的兩個字符是否一樣只需要在T串本身比較就可以了。即T′的前綴T1T2和T′的后綴Tj-2Tj-1比較即可(T':T1T2……Tj-1):    

     判斷T′="abaab"的前綴后綴是否相等,找相等前綴后綴的最大長度 l 

長度為1的:前綴"a",后綴:"b",不等×

長度為2的:前綴"ab",后綴:"ab",相等√

長度為3的:前綴"aba",后綴:" aab",不等×

長度為4的:前綴"abaa",后綴:"baab",不等×

注意:前綴和后綴不可以取字符串本身,如果取了,不就是原地踏步嗎。串的長度為5,前綴和后綴長度最多達到4(l <5)。

相等前綴后綴的最大長度為=2 就可以回退到第+1=3個位置繼續比較了( 不變)。

現在我們可以寫出通用公式,next[j]表示 可以回退的位置,T′="T1T2…Tj-1",則:

 那么我們很容易求出T="abaabe"的next[j]數組:

     解釋1:

j=1:根據公式next[1]=0;

j=2:T′="a",沒有前綴和后綴,next[2]=1;

j=3:T′="ab",前綴為"a",后綴為"b",不等,next[3]=1;

j=4:T′="aba",前綴為"a",后綴為"a",相等且l=1;前綴為"ab",后綴為"ba",不等,next[4]=l+1=2;

j=5:T′="abaa",前綴為"a",后綴為"a",相等且l=1;前綴為"ab",后綴為"aa",不等;前綴為"aba",后綴為"baa",不等,因此next[5]=l+1=2;

j=6:T′="abaab",前綴為"a",后綴為"b",不等;前綴為"ab",后綴為"ab",相等且l=2;前綴為"aba",后綴為"aab",不等;前綴為"abaa",后綴為"baab",不等,取最大長度2,因此next[6]=l+1=3。

這樣找所有的前綴和后綴比較,是不是也是暴力窮舉?那怎么辦呢?Look……

用動態規划遞推一下(數學歸納法,已知n=1成立, 假設n成立,來證明n+1是否成立):

首先大膽假設,已知next[j]=k(求next[j+1]=?)其當前含義是:下一次匹配時, j=next[j]=k, 后判Si==Tk意味着子串中j之前有k-1個字符前后綴匹配,才會有next[j]=k-1+1=k

那么next[j+1]=? 回想 P97 圖4.8,已知 next[j]為表述簡潔,不妨令k=next[j], 如何求next[j+1]?  思考 j=5及j=6:                      

考察以下兩種情況:

      • tj=tk那么 next[ j+1]=next[j]+1=k+1,即,在j+1前,相等前綴和后綴的長度比next[j]=k多1,eg:j=5

      • tjtk當兩者不相等時,我們又開始了這兩個串的模式匹配,找 t next[k] 位置的 tk′比較 (j不變,k變,往前回溯到k'=next[k])。注:程序中的處理,只需要把 next[k] 賦值給 k, k=next[k]不用新變量k' 

          如果tj=tk' next[j+1]=next[k]+1=k'+1;

    如果tjtk',則繼續向前找next[k](k''=next[k']),如果還不相等,繼續向前找next[k′'](k'''=next[k'']),直到找到next[1]=0,停止,此時next[j+1]=next[1]+1=0+1=1,即從第一個字符開始,j=1, eg: j=6

求解next步驟當前j,求j+1
首先位的next值直接賦0,第位的next值直接賦1
其次后面求解每一位的next值時(不妨令求j+1),都要前一位(T.ch[ ])其next值對應位(T.ch[  next[ j ]  ])進行比較。若相等,則該位的next值就是前一位的next值加上1(next[ j ]+1若不等,繼續重復這個過程T.ch[ j ]==T.ch[   next[ next[ j ] ]   ],直到找到相等某一位,將其next值加1即可。如果找到第一位也都沒有找到,那么該位的next值即為1            

      求解next[]的代碼實現如下:

void Get_Next(SString T,int next[])//求模式串T的next[]函數值,其實是知next[1],求next[2],依次……,也即假設已知next[j], ++j后求得next[j]即未我所求;

   {

    next[1] = 0;  //初值

    int k, j;

         j = 1; // j 代表的是后綴末尾的下標, 假設next[j]已知,則 ++j 后,next[j]即是所求

    k = 0; // k代表的是前綴結束時的下標,也就是 前有k個字符的前綴T1T2...Tk等於后綴Tj-m+1...Tj  

       while (j < T.length)  
       { 
       if (k == 0 || T.ch[k] == T.ch[j]) //1. j>=2時, 怎么求next[j+1]?next[3]有前綴后綴,相等->,不等->看上面例子2. 當j=1時,怎么求next[j+1]前綴個數為0->不等<==>k==0后++j;++k;next[j] = k;
        {

           ++j;
           ++k;
          next[j] = k; //++j后才是我們想要的next[j],++k后才是我們想要給next[j]賦的值;一定要先++
        }
          else  //匹配失敗的情況,就要進行回溯,下一輪tjtk'比較,j不動,k'=next[k];
         k= next[k]; 
     }
  }

 用上述方法再次求解求出T="abaabe"的next[]數組:

解釋2:

1. 初始化時next[1]=0,j=1,k=0,進入循環,判斷滿足k==0,則執行next[++j]=++k,即next[2]=1,此時j=2,k=1;

2. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[2]≠T.ch[1],則執行k=next[k],即k=next[1]=0,此時j=2,k=0;

3. 進入循環,判斷滿足k==0,則執行next[++j]=++k,即next[3]=1,此時j=3,k=1;

4. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[3]=T.ch[1],則執行next[++j]=++k,即next[4]=2,此時j=4,k=2;

5. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[4]≠T.ch[2],則執行k=next[k],即k=next[2]=1,此時j=4,k=1;

6. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[4]=T.ch[1],則執行next[++j]=++k,即next[5]=2,此時j=5,k=2;

7. 進入循環,判斷滿足T.ch[j]==T.ch[k],T.ch[5]=T.ch[2],則執行next[++j]=++k,即next[6]=3,此時j=6,k=3;

8. j=T.length,循環結束。

  • 結果是不是和窮舉前綴后綴一模一樣,解釋1(窮舉)同解釋2(歸納)的結果?

有了next[]數組,就很容易進行模式匹配KMP,當S.ch[i]≠T.ch[j]時,j回溯到next[j]的位置 繼續和  S.ch[i]比較即可。

這樣求解非常方便,但也發現有一個問題:當S.ch[i]≠T.ch[j]時,j退回到next[j],然后S.ch[i]與T.ch[k]比較。這樣的確沒錯,但是如果T.ch[j]=T.ch[k]這次比較就沒必要了因為我們剛知道S.ch[i]T.ch[j]啊,

那么肯定S.ch[i]T.ch[k]完全沒必要再比了 (S.ch[i]T.ch[j]=T.ch[k]<==>S.ch[i]≠T.ch[k]).
 再向前找下一個next[],即找可k'=next[k]的位置,繼續比較就可以了。本來應該和第k個位置比較呢,相當於跳到了k的上一個位置k',減少了一次無效比較。

求解next-val步驟( 當前j,求j ):

  1. next-val數組第一個值直接賦0
  2. next-val第二數:模式串第二個字符為B,對應的下標數組第二個數是1,那就是將模式串的第1個字符和B相比較,A!=B,所以直接將下標數組第二個數1作為next-val數組第二個數的值;
  3. 第三個數:模式串第三個字符為A,對應下標數組第三個數為1,取其作為下標,找到模式串第1個字符為A,A=A,那取next-val的第一個數做為next-val第三個數的值,也就是0.
位置
1
2
3
4
5
6
7
模式串
A
B
A
C
A
B
C
next數組
0
1
1
2
1
2
3
nextval數組
0
1
0
2
0
1
3

修改next[]程序:

求解nextval[]的改進代碼實現如下:

 void Get_Nextval(SString T,int nextval[])//求模式串T的nextval[]函數值,在next函數的基礎上只需改動tj=tk相等時的情形,讓nextval[j]=nextval[k]而不是k 

   {  

    nextval[1] = 0; //初值

    int k, j;

         j = 1; // 代表的是后綴末尾的下標, 若next[j]已知,則 ++j 后,next[j]即是所求;

    k = 0;  // k代表的是前綴結束時的下標,也就是 前有k個字符的前綴T1T2...Tk等於后綴Tj-m+1...Tj  

       while (j < T.length)  
       { 
       if (k == 0 || T.ch[k] == T.ch[j]) //1. 當j=1時,怎么求next[2],也即next[++j]?前綴個數為0,特殊值法知next[2]=1<==>k==0;2. j>=2時,已知next[j], 怎么求next[j+1]eg. aaaab
        {        

++j;
++k;
if(T.ch[k]==T.ch[j])
  nextval[j]=nextval[k]; //相等了意味着Si無需和Tk在比較了,回溯到k的上一個位置k'=next[k],讓Si和Sk'比較
else
  nextval[j]=k;  //k其實就是next[j]. 當T.ch[k]!=T.ch[j],相當於nextval[++j]=++k,也即nextval[j]=next[j]=k,此情形同next[]函數;

       }
          else//匹配失敗的情況,就要進行回溯,下一輪tj與tk'比較,j不動,k'=next[k];
         k= nextval[k]; 
     }
  }

 /***KMP及改進算法***/

int Index_KMP(SString S,SString T,int next[])//利用非空模式串T的next函數求T在主串S中的位置的KMP算法,形式上近同BF算法

  {

     int i=1,j=1,sum=0;//i-->S,j-->T,sum-->循環次數
    while(i<=S.length&&j<=T.length)

    {
      sum++;
      if(j==0||S.ch[i]==T.ch[j]) // 繼續比較后面的字符

        {
          i++;
          j++;
        }
      else
        j=next[j]; // 利用next函數確定下一次模式串中第j個字符與T.ch[i]比較,i不變
    }
    cout<<"KMP一共比較了"<<sum<<"次"<<endl;
    if(j>T.length) // 匹配成功
      return i-T.length;
    else
      return 0;
}

 總結: 

BF算法中:當主串和子串不匹配的時候,主串和子串你的指針都須要回溯,因此致使了該算法時間復雜度比較高為 O(n*m)空間復雜度為 O(1)注:雖然其時間復雜度為 O(n*m) 可是在通常應用下執行,其執行時間近似 O(n+m) 因此仍被使用。 

KMP算法:利用子串的結構類似性,設計next數組,在此之上達到了主串不回溯的效果,大大減小了比較次數,可是相對應的卻犧牲了存儲空間,KMP算法時間復雜度為 O(n+m) 空間復雜度為 O(n) 

————————————————
在原文的基礎上,加上自己的理解給大家解釋下,原文鏈接:https://blog.csdn.net/rainchxy/article/details/78130155


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM