BF、KMP、BM、Sunday算法講解


           BFKMPBMSunday算法講解

 

  字串的定位操作通常稱作串的模式匹配,是各種串處理系統中最重要的操作之一。

 

  事實上也就是從一個母串中查找一模板串,判定是否存在。

 

  現給出四種匹配算法包括BF(即二維循環匹配算法)、KMPBMSunday算法,着重講KMP算法,其他算法盡量詳細講解,有興趣的讀者可自行查找其它相關資料了解其它算法,當然本文也會推薦一些網址供讀者參考。

 

  事實上本博文也是作者閱讀了其它博文,然后根據自己的在理解過程中遇到的問題加以闡述,總結而來的,尤其是多次閱讀了July的博文。

 

  本文可能會給一部分讀者閱讀帶來不便,所以在本文開始部分就推薦讀者相關博文,若讀者已經掌握了相關算法,就不要浪費時間繼續瀏覽本博文了,干點其他有意義的事吧:

 

  July博文(強力推薦)http://blog.csdn.net/v_july_v/article/details/7041827

 

 

  注:為方面書寫,S稱作母串,T稱作模板串

 

  一、BF:二維循環匹配算法

     算法比較簡單,不再給予相關解釋,直接給出代碼,如下:

     

 1   int Search(const string& S, const string& T) {
 2   
 3     int i = 0;
 4     int j = 0;
 5   
 6     while(S[i] != ‘\0’ && T[j] != ‘\0’) {
 7       if(S[i] == T[j]) {
 8         ++ i;
 9         ++ j;
10       } else {
11         i = i - j + 1;
12         j = 0;
13       }
14     }
15   
16     if(T[j] == ‘\0’)
17       return i - j;
18   
19     return -1;
20   }

 

     從代碼可以看出,此算法的時間復雜度為O(),相當耗時,一般不采用算法。但是讀者一

  定要明確知道此算法的運行過程,KMP算法就是在此礎上改進而來的。

 

 

  、KMP算法

 

    此算法是D.E.KnuthV.R.Pratt J.H.Morris同時發現的,取三人名字首字母便得到了KMP

  也稱作為克努特-莫里斯-普拉特操作。此算法的時間復雜度為O(n+m)nS的長度,mT的長

  度。

 

    假設S

      BBCABCDABABCDABCDABDE

    T

      ABCDABD

 

    如果按照二維匹配算法,那么過程如下:

 

    第一步,從ST首字符開始匹配

 

      B B C A B C D A B A B C D A B C D A B D E

      ↨

      A B C D A B D

 

    第二步,匹配到此處(為方便說明,省去了不必要的匹配過程)

 

      B B C A B C D A B A B C D A B C D A B D E

            ↨

           A B C D A B D

 

    第三步,根據首字符相匹配可得,可以繼續執行T遍歷,查找S中的子串

 

      B B C A B C D A B A B C D A B C D A B D E

                    ↨

           A B C D A B D

 

    到了此處我們發現出現了不匹配現象,如果按照二維循環匹配算法,那么下一步必然會這樣:

    (1

      B B C A B C D A B A B C D A B C D A B D E

                  ↨

                  A B C D A B D

    緊接着又會出現不匹配,直到運行到下面的結果:

    (2

      B B C A B C D A B A B C D A B C D A B D E

                    ↨

                    A B C D A B D

 

    讓我們算一下,從第三步下標的位置到(2)這一步T的下標共移動了幾次呢??答案是6

  (但願我沒算錯)!!如果我們直接從第三步移動到(2)這一步是不是只需要一次就可以了呢

  ,這樣算下去的話,能減少多少不必要的匹配時間!!

 

    因此,KMP算法誕生了,按照以上的步驟(運行到第三步后直接調轉到2給出相關代碼:

 

 

 1   int KMP_Search(const string& S, const string& T) {
 2   
 3     int i = 0;
 4     int j = -1;
 5     int sLen = S.size();
 6     int tLen = T.size();
 7   
 8     while(i < sLen && j < tLen) {
 9       if(j == -1 || S[i] == T[j]) {
10         ++ i;
11         ++ j;
12       } else {
13         j = next[j];
14       }
15     }
16   
17     if(j == tLen) 
18       return i - j;
19   
20     return -1;
21   }

 

    以上代碼可以看出,關鍵的實現在於next數組,所以讀者會問next數組里面保存了什么,能

  夠實現這么強大的功能??下面將給予解答……

 

    事實上next[k]數組保存了Tk-1位相等的后綴和前綴的最大長度,next[0]-1

    以T”ABCDABD”為例,即:


 

k

前綴

后綴

最長相等的前綴與后綴

next[k]

0

-1

1

0

2

A

B

0

3

AAB

CBC

0

4

AABABC

DCDBCD

0

5

AABABCABCD

ADACDABCDA

A

1

6

AABABCABCDABCDA

BABDABCDABBCDAB

AB

2

7

AABABCABCDABCDAABCDAB

DBDABDDABDCDABDBCDABD

0

    接着,讀者可能會問為什么要保存它??

    不得不說,上述表格中的數據確實說明不了什么,而且還有可能使讀者更加困惑,不過沒關

  系,請耐心繼續往下看,一會就會明白了……

 

 

    借用第三步得到的狀態:

 

      B B C A B C D A B A B C D A B C D A B D E

                    ↨

           A B C D A B D

 

    ST分別匹配到AD處,如果按照二維匹配算法,下一步就會這樣:

 

      B B C A B C D A B A B C D A B C D A B D E

            ↨

         A B C D A B D

 

    S’B’與T’A’匹配辨別,事實上就是(加重部分)的匹配,

 

      B B C A B C D A B A B C D A B C D A B D E

                                 ↨

           A B C D A B D

 

     不成功匹配字符串”BCDABA”中的”BCDAB”正好是T中匹配成功的部分,也就是T

  ”ABCDAB”的字串,也是”ABCDAB”的一個后綴,這個后綴正在與”ABCDAB”進行匹配,

  那么所可能匹配成功的最大長度則是”ABCDAB”刪去最后一個字符得到字符串”ABCDA

  的長度,這個串也是”ABCDAB”的一個前綴,這也就是相當於”ABCDAB”的前綴和后綴在

  匹配,講到這里,讀者應該似乎明白了點什么吧,上述表格似乎有點說法,是吧??那么,

  接着說,既然是一個字符串的前綴和后綴進行匹配,也就是”BCDAB”和”ABCDA”進行匹配,

  即:

 

      B C D A B       B C D A B       B C D A B  

      ↨               ↨               ↨ 

        A B C D A           A B C D A             A B C D A

 

      B C D A B         B C D A B

           ↨                 ↨    

           A B C D A          A B C D A     

 

 

 

    其實以上五個匹配過程也可以這樣看:

 

      B C D A B       C D A B      D A B 

      ↨             ↨        ↨   

        A B C D A           A B C D     A B C 

 

       A B           B

                ↨      

       A B           A

 

    你發現了什么??

 

    好吧,那我說一下我的發現:前綴”ABCDA”一直在與后綴”BCDAB”匹配,如果匹配不成功

  ,那么前綴”ABCDA”的前綴就再與后綴”BCDAB”的后綴繼續匹配……所能匹配成功的最大長度

  就是這一步:

 

      B C D A B

         ↨

           A B C D A

 

    這一步所得到的最大長度就是1,回到next數組,此時k值恰好為5next[5]1,繼續匹

  配將會得到:

 

      B C D A B

            ↨

            A B C D A

 

    此時,k6next[k]2!!

    講到這里,不知道讀者是否明白??我已經盡力讓讀者明白了,如果仍不明白,看來我的表

  述能力存在缺陷,有待提高……

 

    總結起來,就是上述表格所描述的那樣,T的前k-1位的后綴和前綴一直在匹配。

 

    希望讀者能夠好好理解一下上述過程,如果實在不理解,可以留言抑或參考推薦的博文。

 

    OK,下一步用代碼求解next數組。

       

 1   void getNext(const string& T) {
 2   
 3     next[0] = -1;
 4     int i = 0;
 5     int j = -1;
 6   
 7     while(T[i] != ‘\0’) {
 8       if(j == -1 || T[i] == T[j]) {    //    這個if應該難不倒讀者
 9         ++ i;
10         ++ j;
11         next[i] = j;
12       } else {                    //    關鍵在這里,如果不相等,那么j就
13         j = next[j];            //    必須回退
14       }        
15     }        
16       
17   }

 

     到了這里,至於代碼為什么要這么寫需要讀者自己獨立思考一下了……

 

     至於時間復雜度為什么是O(n+m)就不給予證明了,推薦的July博文有明確證明。

 

    注:nS長度,mT長度

  

    下面再給出next數組的優化。

    讀者可能會問next數組為什么需要優化??在這里舉出一個例子進行說明:

 

      假設母串S

        aaabaaaab

      模板串T

        aaaab

 

      當匹配到這里的時候:

 

        a a a b a a a a b

              ↨

          a a a a b

 

 

    按照上述next數組保存的結果進行匹配,可以發現S[3] != T[3],那么下一步就需要根據

  next[3] = 2進行匹配,然后再根據next[2] = 1進行匹配……直到匹配到T的首字符仍然匹配不

  成功為止。那么在求解next數組的時候就可以預判一下,使得在上述匹配不成功的時候直接

  滑向首字符,省去不必要的匹配過程。也就是說在S[i] == T[j] 時,當S[i + 1] == T[j + 1] 時,

  不需要再和T[j]相比較,而是與T[next[j]]進行比較。那么,next數組求解可以改為下述代碼:

    

 1   void getNextval(const string& T) {
 2   
 3     next[0] = -1;
 4     int i = 0;
 5     int j = -1;
 6   
 7     while(T[i] != ‘\0’) {
 8       if(j == -1 || T[i] == T[j]) {
 9         ++ i;
10         ++ j;
11         //    修改部分如下
12         if(T[i] != T[j])
13           next[i] = j;
14         else next[i] = next[j];        
15       } else {                    
16         j = next[j];            
17       }        
18     }            
19   
20   }

 

    ok,整理一下KMP完整代碼:

 

 1   void getNext(const string& T) {
 2     next[0] = -1;
 3     int i = 0;
 4     int j = -1;
 5   
 6     while(T[i] != ‘\0’) {
 7       if(j == -1 || T[i] == T[j]) {
 8         ++ i;
 9         ++ j;
10         next[i] = j;
11       } else {
12         j = next[j];
13       }        
14     }        
15   }
16   
17   void getNextval(const string& T) {
18     next[0] = -1;
19     int i = 0;
20     int j = -1;
21   
22     while(T[i] != ‘\0’) {
23       if(j == -1 || T[i] == T[j]) {
24         ++ i;
25         ++ j;
26   
27         if(T[i] != T[j])
28           next[i] = j;
29         else next[i] = next[j];        
30       } else {                    
31         j = next[j];            
32       }        
33     }            
34   }
35   
36   int KMP_Search(const string& S, const string& T) {
37     int i = 0;
38     int j = -1;
39     int sLen = S.size();
40     int tLen = T.size();
41   
42     while(i < sLen && j < tLen) {
43       if(j == -1 || S[i] == T[j]) {
44         ++ i;
45         ++ j;
46       } else {
47         j = next[j];
48       }
49     }
50   
51     if(j == tLen) 
52       return i - j;
53   
54     return -1;
55   }

 

    ok, kmp算法介紹完畢,下面介紹BM算法~

 

  三、BM算法

 

    事實上KMP算法並不是最快的匹配算法,BM算法(1977年,Robert S.Boyer和J 

  Strother Moore提出)要比KMP算法快,它的時間復雜度為O(n)(平均性能為O(n)

  但有時也會達到O(n * m),而且書寫代碼要復雜,不細心的讀者很容易寫錯),BM算法

  采用從右向左比較的方法,同時應用到了兩種啟發式規則,即壞字符規則 和好后綴規則 

  來決定向右跳躍的距離。

 

    例如:

 

    第一步:

 

      A B C D E F G H I

            ↨

      C D E F G F

 

    然后向前匹配:

 

      A B C D E F G H I

               ↨

        C D E F G F

 

    這就是從后往前匹配。

 

 

    BM算法定義了兩種規則:

 

    注:為方面書寫,S稱作母串,T稱作模板串

    注:規則讀一遍即可,下述圖文解釋匹配過程可完全解釋規則含義。

    注:網頁上關於BM算法的規則說明原理都是一樣的,只不過是表述不同。

 

      A B C D E F G H I

           ↨

        C D E F E F

 

    如上圖所示,藍色字符串“EF”就是好后綴,黃色字符“D”就是壞字符。

 

    1壞字符規則

      在BM算法從右向左掃描的過程中,若發現某個字符x不匹配,則按如下兩種情況討論

      I.如果字符xT中沒有出現,那么從字符x開始的m個文本顯然不可S在此處的字符

    匹配成功,直接全部跳過該區域即可。                 

      II.如果xT中出現,選擇最右該字符進行對齊。 

 

    2好后綴規則

      若發現某個字符不匹配的同時,已有部分字符匹配成功,則按如下兩種情況討論:

      I.如果在T中其他位置存在與好后綴相同的子串,選擇最邊右的子串,將S移使該子

    串與好后綴對齊(相當於T右移)。

      II.如果在T中任何位置不存在與好后綴相同的字串,查找是T中否存在某一前綴與好后

    綴相匹配,如果有選擇最長前綴與S對齊,相當於S左移或者T右移;如果不存在,那么直

    接跳過該后綴,T的首字符與S好后綴的下一字符對齊。

  

    壞字符規則圖文解釋:

     (1)

      A  B  C  D  E  F  G H

           ↨

        H  I   J  K

 

      不匹配,直接跳過,得到:

 

      A  B  C  D  E  F  G  H

                   ↨

            H  I   J  K

  

    (2)

      A B C D E F G

         ↨

      A D D F

 

      字符"D"T中存在,那么得到:

 

      A  B C D E F G

          ↨

        A D D F

 

 

    好后綴規則圖文解釋:

     (1)

      A B C D E F G H I J K

           ↨

        A E F A E F

 

 

      字符"A"與"D"不匹配,好后綴"EF"T中存在,那么得到:

 

      A B C D E F G H I J K

               ↨

           A E F A E F

 

 

    (2)

      A B C D E F G H I J K L

         ↨

      F G F A E F G

 

      T中存在前綴“FG”與后綴“FG”相匹配,那么得到:

 

      A B C D E F G H I J K L

                 ↨

            F G F A E F G

 

 

    如果是這樣:

 

       A B C D E F G H I J K L M N

         ↨

       F A F A E F G

 

      不存在前綴與任一后綴匹配,那么得到:

 

      A B C D E F G H I J K L M N

                    ↨

               F A F A E F G

 

 

    ok,原理說明完畢,下面就是代碼求解:

 

    注:網上諸多作者均給出了詳細求解代碼,可是求解代碼比較晦澀難懂,沒有比較好的注釋

  以幫助讀者完全理解,所以在此根據編者自己的理解,給出了晦澀代碼段的相關注釋。

 

    先給出BM算法的匹配代碼:

 

    注:bmG表示好后綴數組,bmB表示壞字符數組

 

 1   int BM(const string& S, const string& T, int bmG[], int bmB[]) {
 2   
 3     int sLen = S.size() - T.size();
 4     int tLen = T.size();
 5     int i = 0;
 6   
 7     get_bmB(T, bmB);
 8     get_bmG(T, bmG);
 9   
10     while(i <= sLen) {
11       int j = tLen - 1;
12       //    出現不匹配字符或者匹配成功循環結束
13       for( ; j > -1 && S[i + j] == T[j]; --j) ;
14   
15       //    匹配成功
16       if(j == -1)
17         return i;
18   
19       //    選擇好后綴與壞字符中移位最大者,tLen - 1 - j表示的是該字符距字符串尾部的距離,bmB[S[i + j]]表示的是該字符
20       //    出現在T中的最右位置距字符串尾部的距離。
21       i += max(bmG[j], bmB[S[i + j]] - (tLen - 1 - j));
22     }
23   
24     return -1;
25   }

 

    那么,下面就需要求解bmG數組和bmB數組了,由於求解bmG數組比較麻煩,所以先給出

  bmB數組的求解代碼:

 

 1   void get_bmB(const string& T, int bmB[]) {
 2   
 3     int tLen = T.size();
 4   
 5     //    MAXSIZE 表示字符種類數目
 6     //    壞字符不存在T中時,直接后移此片段
 7     for(int i = 0; i < MAXSIZE; ++ i) {
 8       bmB[i] = tlen;
 9     }
10   
11     //    壞字符存在T中時,選擇T中最右字符存在的位置
12     for(int i = 0; i < tLen; ++i) {
13       bmB[T[i]] = tLen - 1 - i;
14     }
15   
16   }

 

    代碼很簡單,讀者稍加理解應該沒問題。

    OK,下面給出bmG數組的求解代碼:

 

 1   //    bmGLength[i]代表的是T在以該字符為后綴的字符串與T的后綴所能匹配的最大長度
 2   int bmGlength[N];
 3   
 4   void get_bmG(const string& T, int bmG[]) {
 5   
 6     //    求解好后綴數組那么必先得到匹配不成功處好后綴的長度是多少
 7     get_bmGLength(T, bmGLength);
 8   
 9     //    那么按照好后綴的原理,先默認選擇匹配不成功之處不存在好后綴的情況
10     int tLen = T.size();
11     for(int i = 0; i < tLen; ++ i) {
12       bmG[i] = tLen;
13     }
14   
15   
16     //    注:以下代碼中i之所以 < tLen - 1是因為下標tLen - 1處不存在好后綴,只有壞字符
17   
18     //    然后是選擇匹配不成功之處T中存在包含該字符的前綴與好后綴相匹配的情況
19     int j = 0;
20     for(int i = tLen - 2; i >= 0; -- i) {
21       //    若存在前綴與后綴相匹配,那么此處所保存的數值必然是i + 1
22       if(bmGLength[i] == i + 1) {
23         for(; j < tLen - 1 - i; ++ j) {
24           //    還沒有變化過時方可賦值
25           if(bmG[j] == tLen)
26             bmG[j] = tLen - 1 - i;
27         }
28       }
29     }
30   
31     //    最后就是不匹配處T中存在與好后綴相匹配的字符串,選擇最右
32     //    字符串
33     for(int i = 0; i < tLen - 1; ++ i) {    
34       bmG[tLen - 1 - bmGLenth[i]] = tLen - 1 - i;
35     }
36   
37   }

 

     那么接着給出bmGLength數組的求解代碼:

    注:代碼中有很多地方尤其是數組下標中加了相關括號,這是方便讀者理解的地方,希望

  讀者要稍加注意

 

 1   void get_bmGLength(const string& T, int bmGLength[]) {
 2     int tLen = T.size();
 3     bmGLength[tLen - 1] = tLen;
 4     for(int i = tLen - 2; i >= 0; -- i) {
 5       int j = i;
 6   
 7       while(j >= 0 && T[j] == T[tLen - 1 - (i - j)])
 8         -- j;
 9   
10       bmGLenth[i] = i - j;
11     }
12   
13   }

 

    不難發現,此代碼的時間復雜度為O(n2),事實上我們可以優化一下,優化成O(n)

  下見代碼:

 

    為方便讀者理解,先說一下以下代碼的原理:

    如果i所到的最遠處curpre不等,那么就存在最小循環節在[cur, pre][cur, tLen - 1]中,

  這個最小循環節內部(指除去最右字符剩下的字符)的字符必然不是完全匹配(也就是從此

  字符開始與后綴進行匹配,必然匹配不到一個循環節的長度),這些不完全匹配的字符所能

  匹配的最大長度在每個循環節中必然相同;而完全匹配的字符(也就是最右字符)所能匹配

  的最大長度的差值必然是循環節的整數倍。

 

    不知道這樣說對不對,若有不對之處還請讀者指正,若在理還請讀者細細品味。

    這個原理也僅是讀者通過代碼進行理解而得到的,至於原來最先想出此優化代碼的程序員已

  不可考,其根本原理也已不可知。

 

 1   void get_bmGLength(const string& T, int bmGLength[]) {
 2     int tLen = T.size();
 3     bmGLength[tLen - 1] = tLen;
 4   
 5     int cur = tLen - 1;    //    保存當前下標i所到的最遠位置
 6     int pre;    //    保存下標i先前的位置
 7   
 8     for(int i = tLen - 2; i >= 0; -- i) {
 9   
10     //    i > cur 是因為i必須要曾經遍歷到過下標cur方可
11     //    (tLen - 1 - pre)表示先前i到T尾字符的長度
12     //    bmGLength[i + (tLen - 1 - pre)] < i - cur這個條件為什么要加上,
13     //    我也不明白,不過去掉這個條件,按照原理是成立的,加上也沒有錯,給出一個題目鏈接,讀者可以測試一下編者說的是否正確
14     //    http://acm.hdu.edu.cn/showproblem.php?pid=1711
15 
16     /*    網上源代碼:
17       if(i > cur && bmGLength[i + (tLen - 1 - pre)] < i - cur) {
18         bmGLength[i] = bmGLength[i + (tLen - 1 - pre)];
19         continue;
20       }
21     */
22     //    原理代碼:
23       if(i > cur) {
24         bmGLength[i] = bmGLength[i + (tLen - 1 - pre)];
25         continue;
26       }
27       //    i所到的最遠的位置必然是i最小時
28       cur = min(cur, i);
29       pre = i;
30   
31       while(cur >= 0 && T[cur] == T[tLen - 1 - (pre - cur)])
32         -- cur;
33   
34       bmGLenth[i] = pre - cur;
35     }
36   
37   }
38   

 

    OK,講解完畢,下面給出BM完整代碼:

 

 1   void get_bmB(const string& T, int bmB[]) {
 2     int tLen = T.size();
 3     for(int i = 0; i < MAXSIZE; ++ i) {
 4       bmB[i] = tlen;
 5     }
 6 
 7     for(int i = 0; i < tLen; ++i) {
 8       bmB[T[i]] = tLen - 1 - i;
 9     }
10   }
11 
12   void get_bmGLength(const string& T, int bmGLength[]) {
13     int tLen = T.size();
14     bmGLength[tLen - 1] = tLen;
15     for(int i = tLen - 2; i >= 0; -- i) {
16       int j = i;
17       while(j >= 0 && T[j] == T[tLen - 1 - (i - j)])
18         -- j;
19   
20       bmGLenth[i] = i - j;
21     }
22   }
23   
24   void get_bmGLength(const string& T, int bmGLength[]) {
25     int tLen = T.size();
26     bmGLength[tLen - 1] = tLen;
27   
28     int cur = tLen - 1;
29     int pre;
30   
31     for(int i = tLen - 2; i >= 0; -- i) {
32     /*    網上源代碼:
33       if(i > cur && bmGLength[i + (tLen - 1 - pre)] < i - cur) {
34         bmGLength[i] = bmGLength[i + (tLen - 1 - pre)];
35         continue;
36       }
37     */
38     //    原理代碼:
39       if(i > cur) {
40         bmGLength[i] = bmGLength[i + (tLen - 1 - pre)];
41         continue;
42       }
43   
44       cur = min(cur, i);
45       pre = i;
46       while(cur >= 0 && T[cur] == T[tLen - 1 - (pre - cur)])
47         -- cur;
48       bmGLenth[i] = pre - cur;
49     }
50   }
51   
52   int bmGlength[N];
53   
54   void get_bmG(const string& T, int bmG[]) {
55   
56     get_bmGLength(T, bmGLength);
57   
58     int tLen = T.size();
59     for(int i = 0; i < tLen; ++ i) {
60       bmG[i] = tLen;
61     }
62   
63     int j = 0;
64     for(int i = tLen - 2; i >= 0; -- i) {
65       if(bmGLength[i] == i + 1) {
66         for(; j < tLen - 1 - i; ++ j) {
67           if(bmG[j] == tLen)
68             bmG[j] = tLen - 1 - i;
69         }
70       }
71     }
72   
73     for(int i = 0; i < tLen - 1; ++ i) {    
74       bmG[tLen - 1 - bmGLenth[i]] = tLen - 1 - i;
75     }
76   }
77 
78   int BM(const string& S, const string& T, int bmG[], int bmB[]) {
79 
80     get_bmB(T, bmB);
81     get_bmG(T, bmG);
82   
83     int sLen = S.size() - T.size();
84     int tLen = T.size();
85     int i = 0;
86   
87     while(i <= sLen) {
88       int j = tLen - 1;
89       for(; j > -1 && S[i + j] == T[j]; --j) ;
90 
91       if(j == -1)
92         return i;
93   
94       i += max(bmG[j], bmB[S[i + j]] - (tLen - 1 - j));
95     }
96     return -1;
97   }

 

    okBM算法介紹完畢,下面介紹Sundy算法,最簡單的算法。

 

  四、Sunday算法

 

    Sunday算法是Daniel M.Sunday1990年提出的字符串模式匹配。相對比較KMPBM

  算法而言,簡單了許多。

    原理與BM算法相仿,有點像其刪減版,所以其時間復雜度和BM算法差不多,平均性能的

  時間復雜度也為O(n),最差情況的時間復雜度為O(n * m),但是要容易理解。

 

    匹配原理:從前往后匹配,如果遇到不匹配情況判斷母串S參與匹配的最后一位的下一位字符

  ,如果該字符出現在模板串T中,選擇最右出現的位置進 行對齊;否則直接跳過該匹配區域。

 

    原理看着都這么繁瑣,而且難懂,還是給讀者上圖吧:

 

    母串S

      S  E  A  R  C  H  S  U  B  S  T  R  I  N  G

 

    模板串T

      S  U  B  S  T  R  I  N  G

 

    開始匹配:

 

      S  E  A  R  C  H  S  U  B  S  T  R  I  N  G

      ↨

      S  U  B  S  T  R  I  N  G

 

    繼續下一字符匹配:

 

      S  E  A  R  C  H  S  U  B  S  T  R  I  N  G

        ↨

      S  U  B  S  T  R  I  N  G

 

    出現不匹配情況,查找母串參與匹配的最后一位字符的下一字符,上圖中S中最后一位參與

  匹配的字符是顏色為藍色的字符B’,其下一字符為’S’,在T中,字符’S’出現兩次,按照原理,

  選擇最右位置出現的’S’進行對齊,那么可以得到:

 

      S  E  A  R  C  H  S  U  B  S  T  R  I  N  G

               ↨

                 S  U  B  S  T  R  I  N  G

 

    直接跳過大片區域。

 

    假設母串S為:

      S  E  A  R  C  H  S  U  B  Z  T  R  I  N  G

 

    那么當匹配到上述情況時,字符’Z’在T中沒有出現,那么就可以得到下面的情況:

 

      S  E  A  R  C  H  S  U  B  Z  T  R  I  N  G

                              ↨

                           S  U  B  S  T  R  I  N  G

 

    跳過的區域很大吧。

    ok,這就是其原理的兩種情況,很簡單吧,下面給出代碼解釋:

    注:S表示母串,T表示模板串

 

 1   int moveLength[MAXSIZE];    //    匹配不成功時的移動步長,默認初始化
 2   
 3   int Sunday(const string& S, const string &T) {
 4     getMoveLength(T);
 5   
 6     int tLen = T.size();
 7     int sLen = S.size();
 8     int i = 0;    //    S遍歷下標
 9     while(i < sLen) {
10       int j = 0;
11       //    符合條件下標就繼續右移
12       for(  ; j < tLen && i + j < sLen && S[i + j] == T[j]; ++ j) ;
13       //    遍歷結束,判斷遍歷情況
14       if(j >= tLen) return i;
15       //    查找不成功,那么S下標右移
16       if(i + tLen >= sLen) 
17       return -1;
18       i += moveLength[S[i + tLen]];
19     }
20   
21     return -1;
22   }

 

    匹配過程很簡單,相信讀者很容易理解。

    那么,需要求解moveLength數組,下面給出其求解代碼:

 

 1   int MAXSIZE = 256;    //    字符串種類數,視情況而定
 2 
 3   void getMoveLength(const string &T) {
 4     int tLen = T.size();
 5     //    默認S中的任何字符均不出現在T中,那么每次移動的距離為T的長度 + 1
 6     for(int i = 0; i < MAXSIZE; ++ i)
 7       moveLength[i] = tLen + 1;
 8   
 9     //    查找能夠出現在T中的字符,若一個字符出現多次,選擇最右位置的字符,所以T的下標遍歷從0開始
10     for(int i = 0; T[i]; ++ i)
11       moveLength[T[i]] = tLen - i;
12   }
13   

 

    ok,代碼解釋完畢,非常簡單。

    下面給出完整代碼:

 

 1   int MAXSIZE = 256;
 2   int moveLength[MAXSIZE];
 3 
 4   void getMoveLength(const string &T) {
 5     int tLen = T.size();
 6     for(int i = 0; i < MAXSIZE; ++ i)
 7       moveLength[i] = tLen + 1;
 8   
 9     for(int i = 0; T[i]; ++ i)
10       moveLength[T[i]] = tLen - i;
11   }
12   
13   int Sunday(const string& S, const string &T) {
14     getMoveLength(T);
15   
16     int tLen = T.size();
17     int sLen = S.size();
18     int i = 0;    
19     while(i < sLen) {
20       int j = 0;
21       for(  ; j < tLen && i + j < sLen && S[i + j] == T[j]; ++ j) ;
22   
23       if(j >= tLen) return i;
24       if(i + tLen > sLen) 
25         return -1;
26       i += moveLength[S[i + tLen]];
27     }
28   
29     return -1;
30   }

 

  BFKMPBMSunday算法均介紹完畢,若讀者有不明之處抑或博文有誤之類,請盡情留言,編者看到后會及時回復及修改博文。

 

  希望讀者理解四種算法之后能夠自己獨立手寫其核心代碼,對讀者加深印象以及其核心原理很有裨益。

 

  最后再次列舉推薦博文以及相關書籍(當然也曾讀到過一些博文以及書籍,從編者角度來說不易理解,便沒有推薦,還望見諒):

 

    July博文(再次推薦)  http://blog.csdn.net/v_july_v/article/details/7041827

    算法導論(第三版),機械工業出版社,Thomas H. Cormen ... 著,殷建平...

 

 

  暫時推薦上面比較稀少的博文與書籍,若有讀者能夠推薦給編者比較好的博文及書籍抑或日后看到,定會添加於上。

 

                                            2014115

 


免責聲明!

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



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