字符串類——KMP子串查找算法


1, 如何在目標字符串 s 中,查找是否存在子串 p(本文代碼已集成到字符串類——字符串類的創建(上)中,這里講述KMP實現原理) ?

       1,朴素算法:

             

  2,朴素解法的問題:

             

            

              1,問題:有時候右移一位是沒有意義的;

              2,KMP 算法可以右移一定的位數,提高效率;

    3,朴素算法和 KMP 算法對比示例圖:

                    

 

2,偉大的發現(KMP):

       1,匹配失敗時的右移位數與子串本身相關,與目標無關;

       2,移動位數 = 已匹配的字符數 - 對應的部分匹配值;

              1,“已匹配的字符數”已知,“對應的部分匹配值”未知;

           (2),部分匹配值就是對應元素和從開始元素開始連續相同的個數;

       3,任意子串都存在一個唯一的部分匹配值;

 

3,部分匹配表示例:

 

      

4,部分匹配表如何獲得 ?

       1,前綴集:

              1,除了最后一個字符外,一個字符串的全部頭部組合;

       2,后綴集:

              1,除了第一個字符以外,一個字符串的全部尾部組合;

       3,部分匹配值:

              1,前綴集和后綴集最長共有元素的長度;

            (2),得到共有長度是為了得到對應各個位置前面不相同的元素個數,這樣如果前面不同元素匹配了,那么就可以直接移動的了;

 

4,ABCDABD 部分匹配表示例:

 

                    

5,怎么編程產生部分匹配表(Partial Matched Table)(遞推完成)?

       1,實現關鍵:

              1,PMT[1] = 0(下標為 0 的元素匹配值為 0);

              2,從 2 個字符開始遞推(從下標為 1 的字符開始遞推);

              3,假設 PMT[n] = PMT[n-1] + 1(最長共有元素的長度)(這是一個貪心假設);

              4,當假設不成立,PMT[n] 在 PMT[n-1](這里是指的第 PMT[n-1] 個元素)個元素的 ll 值上用種子作為擴展的基礎上繼續比對;

                     1,當當前前子集“前后集最長共有元素數”為 0 時,說明其元素都不相等,可以直接比較當前子集延長一字母序列的首尾字母,且此子集“前后集最長共有元素數”最大為 1;

                     2,將 “前后集最長共有元素數”對應的前后綴最為種子擴展;

                     3,假設不成立時,把已經匹配的前 PMT[n-1] 個元素的“前后集最長共有元素數”(因為必須在相同的位置上擴展才有意義)作為種子來擴展;

 

6,部分匹配表的遞推與實現:

  1,部分匹配表的遞推:

  

  2,在 String 中實現部分匹配表:

 1 /* 建立指定字符串的 pmt(部分匹配表)表 */
 2 int* String::make_pmt(const char* p)  //  O(m),只有一個 for 循環
 3 {
 4     int len = strlen(p);
 5 int* ret = static_cast<int*>(malloc(sizeof(int) * len));
 6 
 7     if ( ret != NULL )
 8     {
 9         int ll = 0; //定義 ll,前綴和后綴交集的最大長度數,largest length;第一步
10         ret[0] = 0;  // 長度為 1 的字符串前后集都為空,對應 ll 為 0;
11 
12         for(int i=1; i<len; i++)  // 從第一個下標,也就是第二個字符開始計算,因為第 0 個字符前面已經計算過了; 第二步
13         {
14             /* 算法第四步 */
15             while( (ll > 0) && (p[ll] != p[i]) ) // 當 ll 值為零時,轉到下面 if() 函數繼續判斷,最后賦值與匹配表,所以順序不要錯;
16             {
17                 ll = ret[ll - 1];  // 從之前匹配的部分匹配值表中,繼續和最后擴展的那個字符匹配
18             }
19 
20             /* 算法的第三步,這是成功的情況 */
21             if( p[ll] == p[i] ) // 根據 ll 來確定擴展的種子個數為 ll,而數組 ll 處就處對應的擴展元素,然后和最新擴展的元素比較;
22             {
23                 ll++;   // 若相同(與假設符合)則加一
24             }
25 
26             ret[i] = ll;   // 部分匹配表里存儲部分匹配值 ll
27         }
28 }
29 
30     return ret;
31 }

                       

7,部分匹配表的使用(KMP 算法):

      

       1,不匹配時,移動位置,之后直接從/字符串的/不匹配前字符/的部分匹配值下標處/開始匹配;

      

8,KMP 子串查找算法在 String 中的實現 :

 1 /* 在字符串 s 中查找子串 p */
 2 int String::kmp(const char* s, const char* p)  // O(m) + O(n) ==> O(m+n), 只有一個 for 循環
 3 {
 4     int ret = -1;
 5     int sl = strlen(s);
 6     int pl = strlen(p);
 7    int* pmt = make_pmt(p);
 8 
 9     if( (pmt != NULL) && (0 < pl) && (pl <= sl) ) // 判斷查找條件
10     {
11         for(int i=0, j=0; i<sl; i++)  // i 的值要小於目標竄長度才可以查找
12         {
13             while( (j > 0) && (s[i] != p[j]) ) // 比對不上的時候,持續比對,
14             {
15                 j = pmt[j-1];//移動后應該繼續匹配的位置,j =j-(j -LL)=LL = PMT[j-1]
16             }
17 
18             if( s[i] == p[j] )  // 比對字符成功
19             {
20                 j++;   // 加然后比對下一個字符
21             }
22 
23             if( j == pl )  // 這個時候是查找到了,因為 j 增加到了 pl 的長度;
24             {
25                 ret = i + 1 - pl; // 匹配成功后,i 的值停在最后一個匹配成功的字符上,這樣就返回匹配成功的位置
26 
27                 break;
28             }
29         }
30    }
31 
32    free(pmt);
33 
34     return ret;
35 }

 

9,小結:

       1,部分匹配表是提高子串查找效率的關鍵;

       2,部分匹配值定義為前綴和后綴最長共有元素的長度;

       3,可以用遞推的方法產生部分匹配表;

       4,KMP 利用部分匹配值與子串移動位數的關系提高查找效率;

              1,每次匹配失敗的時候,子串不會簡單的右移一位,而是查詢部分匹配表中的值,查到后則右移一定位數,使算法效率由平方變成線性時間;


免責聲明!

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



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