算法之旅——KMP模式匹配

       模式匹配是數據結構中的基本運算之一,它在很多地方都得到了應用,字符串模式匹配指的是:找出模式串在一個較長的字符串中出現的位置。有兩個字符串target 和pattern,字符串target 稱爲目標串,字符串pattern 稱爲模式串,要求找出pattern在target 中首次出現的位置。一旦模式串pattern 在目標串target 中找到,就稱發生一次匹配。例如,目標串target=“banana”,模式串pattern=“nan",則匹配結果是2。常用的模式匹配方法有樸素模式匹配和KMP模式匹配。樸素模式匹配法很容易理解,就是將pattern和target一個一個字符比對,如果這個字符匹配了,則pattern和target都後移一位,繼續比對下一個字符;如果這個字符不匹配,則pattern起始位置歸零,target的起始位置移到上次匹配起始位置的下一位,然後重新開始新一輪的匹配。這裏舉個例子來簡要說明樸素模式匹配的過程:設目標串target=”ababcabcac",模式串pattern=“abcac",設置兩個指針i,j分別指向target和pattern,開始時,i指向target[0],j指向pattern[0],顯然pattern[0] 與target[0]匹配,i和j都後移一位,i指向target[1],j指向pattern[1], pattern[1] 與target[1]也匹配,i和j又都後移一位,i指向target2],j指向pattern[2], 但是pattern[2] 與target[2]不相等,也就不匹配了,至此這一輪的匹配結束,開始下一輪的匹配過程。


        常用的模式匹配方法有樸素模式匹配和KMP模式匹配。樸素模式匹配法很容易理解,就是將pattern和target一個一個字符比對,如果這個字符匹配了,則pattern和target都後移一位,繼續比對下一個字符;如果這個字符不匹配,則pattern起始位置歸零,target的起始位置移到上次匹配起始位置的下一位,然後重新開始新一輪的匹配。這裏舉個例子來簡要說明樸素模式匹配的過程:
設目標串target=”ababcabcac",模式串pattern=“abcac",設置兩個指針i,j分別指向target和pattern,開始時,i指向target[0],j指向pattern[0],顯然pattern[0] 與target[0]匹配,i和j都後移一位,i指向target[1],j指向pattern[1], pattern[1] 與target[1]也匹配,i和j又都後移一位,i指向target2],j指向pattern[2], 但是pattern[2] 與target[2]不相等,也就不匹配了,至此這一輪的匹配結束,開始下一輪的匹配過程。

  I = 0    

   1      

      2    

     3    

     4    

     5    

     6    

    7   

     8   

    9   

    a

    b

     a

    b

   c

    a

    b

   c

   a

   c

 J = 0

   1

    2

    3

    4

    5

    6

   7

   8

  9

    a

    b

    c

    b

    c

 

 

 

 

 

         下一輪匹配過程開始時,i從上一次匹配開始的位置0後移一位,那這裏i從1開始,j已然要從0開始,因爲target[1]不等於pattern[0],不匹配,那麼ij要從起始位置後移一位。

     0   

   i = 1    

      2    

     3    

     4    

     5    

     6    

    7   

     8   

    9   

    a        

    b      

     a       

    b        

   c        

    a        

    b       

   c       

   a        

   c       

 

   J = 0

   1

    2

    3

   4

    5

  6

   7

    8

  

  a

    b

   c

    a

    c

 

 

 

 


       開始第三輪匹配,此時i=2,j=0. pattern[0]=target[2], pattern[1]=target[3],pattern[2]=target[4],pattern[3]=target[5],q前面幾個都字符匹配,但是pattern[4]!=target[6],失配,因此繼續下一輪匹配,i從起始位置後移一位,移到3上

     0    

   1      

   i =  2    

     3    

     4    

     5    

     6    

    7   

     8   

    9   

    a

    b       

     a       

     b       

    c        

     a       

     b       

    c       

    a       

    c      

 

   

    j =  0

     1

     2

     3

    4

    5

     6

    7

   

   

      a

     b

    c

    a

     c

 

 

 


開始第四輪匹配,此時i=3,j=0,

     0    

   1      

     2    

   i =  3    

     4    

     5    

     6    

    7   

     8   

    9   

    a

    b       

     a       

     b       

    c        

     a       

     b       

    c       

    a       

    c      

 

   

   

    j =  0

     1

     2

    3

    4

     5

    6

   

   

     

      a

     b

     c

     a

    c

 

 


重複上述的匹配過程,這裏不再贅述。。。

     0    

   1      

     2    

  3    

     4    

     i =  5   

     6    

    7   

     8   

    9   

    a

    b       

     a       

     b       

    c        

     a       

     b       

    c       

    a       

    c      

 

   

   

   

     

    j =  0

    1

    2

    3

    4

   

   

     

     

   

    a

     b

 c

 a

  c


       由此可以看出,雖然樸素模式匹配最終能找到模式串在目標串中的位置,但是匹配的過程過於繁瑣,例如在第三輪匹配過程中,前四個字符恰好匹配,但第五個字符不匹配,導致第四次匹配過程又要重頭開始,效率過於低下。
這裏給出一份樸素模式匹配的代碼,供大家參考 (此代碼已編譯通過,正常運行)

  1. //description:樸素模式匹配算法  
  2. //author:hust_luojun  
  3. //data:2014-7-17  
  4.   
  5. #include <iostream>  
  6. #include <cstring>  
  7.   
  8. using namespace std;  
  9.   
  10. int main()  
  11. {  
  12.     int simple_pattern_matching(char target[],char pattern[]);  
  13.     char target[]={"ababcabcacb"};                              //target爲目標串  
  14.     char pattern[]={"abcac"};                                   //pattern爲模式串  
  15.   
  16.     int pos=simple_pattern_matching(target,pattern);  
  17.     cout<<"target string:   "<<target<<endl;  
  18.     cout<<"pattern string:  "<<pattern<<endl;  
  19.     cout<<"the postition of pattern is:  "<<pos<<endl;  
  20.     return 0;  
  21. }  
  22.   
  23. int simple_pattern_matching(char target[],char pattern[])  
  24. {  
  25.     int i=0;  
  26.     int j=0;  
  27.     int target_length=strlen(target);               //目標串的長度  
  28.     int pattern_length=strlen(pattern);             //模式串的長度  
  29.   
  30.     while(i<target_length && j<pattern_length)  
  31.     {  
  32.         if(target[i]==pattern[j])                   //逐位比較,若相等則比較下一位  
  33.         {  
  34.             i++;  
  35.             j++;  
  36.         }  
  37.         else                                        //逐位比較,若不等則i從上一次開始比較的位置加1,j取0  
  38.         {  
  39.             i=i-j+1;  
  40.             j=0;  
  41.         }  
  42.     }  
  43.     if(j==pattern_length)                           //若j等於模式串的長度,說明匹配成功,返回模式串在目標串中的位置,若匹配不成功則返回-1  
  44.         return i-j;  
  45.     else  
  46.         return -1;  
  47. }  
此樸素模式匹配運行結果如下:



        樸素模式匹配算法過程易於理解,在很多應用場合效率也較高。此時,算法的時間複雜度爲O(n+m)。其中n和m分別爲目標串和模式的長度。然而,當目標串中存在多個和模式串中“部分匹配”的子串時,會引起指針i的多次回溯。因此,在最壞情況下的時間複雜度爲O(n*m)。那麼有沒有更好的匹配方法呢?當然有,就是接下來要介紹的KMP模式匹配方法。
KMP算法是一種可以在時間複雜度O(n+m)完成匹配查找的算法,它相對於樸素匹配算法的改進在於:當某一趟匹配失敗時,i不需要回溯到上次開始匹配位置的下一位,而是停留在失配時的那一位上,j也不用回溯到0,而是回溯到一個儘量靠右的位置,爲了找出這個儘量靠右的位置,這裏引入next[  ]數組,k=next[j], j爲模式串失配時的指向,在pattern[0....j-1]內找出滿足pattern[0.....k-1]==pattern[j-k....j-1] 的最大k值,下次匹配時j就回溯到k位置上。


       樸素匹配那個例子中,進行第三趟匹配時,在最後一個字符上失配,此時i=6,j=4,進行第四趟匹配時,若採用KMP算法,i不需要從3開始,j不需要從0開始。由於模式串的前四個字符已經匹配,可以得知target[3,4,5]=="bca",故可以利用這個部分匹配串的信息,pattern[0]==a,不可能target[3],target[4]匹配,但可以跟target[5]匹配,不過這個匹配或不匹配的信息是可以由第三趟匹配過程得知的,故第四趟匹配時,i可以直接爲6,j直接爲2,這樣就跳過了中間一些不必要的比較過程,節省了時間。

KMP匹配過程如下:

第一趟:

    a    

   b    

   a   

   b     

    c  

    a   

    b     

     c   

    a     

    c     

   a

  b

  c

    a

   c

 

 

 

 

 

 第二趟:

    a    

   b    

   a   

   b     

    c  

    a   

    b     

     c   

    a     

    c     

   

  a

  b

  c

   a

 c

 

 

 

 


第三趟:

    a    

   b    

   a   

   b     

    c  

    a   

    b     

     c   

    a     

    c     

   

  

   a

    b

    c

    a

    c

 

 

 


第四趟:

    a    

   b    

   a   

   b     

    c  

    a   

    b     

     c   

    a     

    c     

   

 

  

    

   

   a

   b

    c

    a

    c



在第四趟時就完成匹配了。下一次匹配時,j的起始比較位置j=k=next[j],那麼如何求next[j]呢?這裏直接給出公式,有興趣的童鞋可以自己推到一下

next[j]=-1 (j=0);

next[j]=0  (j=1);

next[j]=max{k}  (其中k爲滿足下式的最大值 pattern[0....k-1]==pattern[j-k....j-1])

可見next[j]只於模式串有關,而與目標串無關

Pattern[j]

        

         

         

         

         

          

          

           

          

     a

  b

   a

   b

   c

   a

   b

   c

    a

    c

  Next[j]

 

 

 

 

 

 

 

 

 

   -1

  0

  0

  1

   2

   0

  1

   2

    0

   1


因此KMP算法的思想就是:在匹配過程中,若發生不匹配的情況,如果next[j] ≥ 0,則目標串的指針i不變,將模式串的指針j 移動到next[j]的位置繼續進行匹配;如果next[j] == -1,則將i右移1位,並將j置0,繼續進行比較。

下面給出一份KMP算法的參考代碼(此代碼已編譯通過,正常運行),若大家有更好的實現代碼,歡迎分享,共同學習

  1. //description:KMP模式匹配算法  
  2. //author:hust_luojun  
  3. //data:2014-7-17  
  4.   
  5. #include <iostream>  
  6. #include <cstring>  
  7.   
  8. using namespace std;  
  9.   
  10. int main()  
  11. {  
  12.     int kmp_pattern_matching(char target[],char pattern[]);     //KMP函數聲明  
  13.     char target[]={"ababcabcacb"};                              //target爲目標串  
  14.     char pattern[]={"abcac"};                                   //pattern爲模式串  
  15.   
  16.     int pos=kmp_pattern_matching(target,pattern);                //調用KMP函數,返回模式串的位置  
  17.     cout<<"target string:   "<<target<<endl;  
  18.     cout<<"pattern string:  "<<pattern<<endl;  
  19.     cout<<"the postition of pattern is:  "<<pos<<endl;  
  20.     return 0;  
  21. }  
  22.   
  23. int kmp_pattern_matching(char target[],char pattern[])  
  24. {  
  25.     int get_next(char pattern[]);           //  求next[j]大小的函數聲明  
  26.     int i = 0;  
  27.     int j = 0;  
  28.     int target_length = strlen(target);     //目標串的長度  
  29.     int pattern_length = strlen(pattern);   //模式串的長度  
  30.   
  31.     while(i < target_length && j < pattern_length)  
  32.     {  
  33.         if(target[i]==pattern[j] || j==-1)      //逐個字符匹配,若相等則i和j分別後移一位  
  34.         {  
  35.             i++;  
  36.             j++;  
  37.         }  
  38.         else  
  39.         {  
  40.             j = get_next(pattern);              //逐個字符匹配,若不相等,i保持失配使的值,j的值取next[j]  
  41.         }  
  42.     }  
  43.     if(j==pattern_length)                       //當j等於模式串的長度時,說明匹配成功,返回模式串在目標串中的位置  
  44.         return i-j;  
  45.     else  
  46.         return -1;                              //匹配不成功時,返回-1  
  47. }  
  48.   
  49. int get_next(char pattern[])                    //求next[j]數組,本函數中,用k替代了next[j],但本質是相同的,k=next[j]  
  50. {  
  51.     bool compare(char pattern[],int k,int j);  
  52.     int pattern_length = strlen(pattern);  
  53.     int j;  
  54.     int k;  
  55.     int temp;  
  56.     for(j=0;j<pattern_length;j++)  
  57.     {  
  58.         if(j==0)                                          //next[0]==-1; next[1]=0  
  59.             k=-1;  
  60.         else if(j==1)  
  61.                 k=0;  
  62.              else  
  63.              {  
  64.                 for(temp=j-1;temp>=1;temp--)            //求next[j] (j>=2)的最大k值  
  65.                 {  
  66.                     if (compare(pattern,temp,j))  
  67.                         k=temp;  
  68.                         break;  
  69.                 }  
  70.              }  
  71.         return k;  
  72.     }  
  73. }  
  74.   
  75. bool compare(char pattern[],int k,int j)  
  76. {  
  77.     int s=0;  
  78.     int t=j-k;  
  79.     for(;s<=k-1&&t<=j-1;s++,t++)         //比較pattern[0....k-1]與pattern[j-k....j-1]是否相等,相等則返回true,不等則返回false  
  80.         if(pattern[s]!=pattern[t])  
  81.         return false;  
  82.         else  
  83.         return true;  
  84. }  

運行結果如下:



大家可以在截圖比較一下,在同一個目標串中查找同一個模式串,樸素匹配算法運行時間爲0.333s, KMP算法運行時間爲0.005s,時間效率大爲提升。

本文參考了張曉芳老師文章的部分內容,在此表示感謝!


/*****碼字不易,轉載請註明出處*****/


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