字符串模式匹配算法1 - BF和KMP算法


 

在字符串S中定位/查找某個子字符串P的操作,通常稱為字符串的模式匹配,其中P稱為模式串。模式匹配有多種算法,這里先總結一下BF算法和KMP算法。

注意:本文在討論字符位置/指針/下標時,全部使用C語法,即下標從0開始。

BF算法

BF(Brute Force)算法也就是傳說中的“笨辦法”,是一個暴力/蠻力算法。設串S和P的長度分別為m,n,則它在最壞情況下的時間復雜度是O(m*n)。BF算法的最壞時間復雜度雖然不好,但它易於理解和編程,在實際應用中,一般還能達到近似於O(m+n)的時間度(最壞情況不是那么容易出現的,RP問題),因此,還在被大量使用。

下面舉例來說明BF算法的思想。

設S=‘ababcabcacbab’, P=‘abcac’,從S的第1個字符開始,依次比較S和P中的字符,如果沒有完全匹配,則從S第2個字符開始,再次比較...如此重復,直到找到P的完全匹配或者不存在匹配。用數學語言描述,就是比較SiSi+1...Si+n-1和P0P1...Pn-1,如果出現不匹配,則令i=i+1,繼續這一過程,直到全部匹配,或者i>(m-n)。匹配過程如下(紅色字體表示本趟比較中不匹配的字符):

第1趟
S: a b a b c a b c a c b a b
P: a b c 

第2趟
S: a b a b c a b c a c b a b
P:   a

第3趟
S: a b a b c a b c a c b a b
P:     a b c a c

第4趟
S: a b a b c a b c a c b a b
P:       a

第5趟

S: a b a b c a b c a c b a b
P:         a

第6趟

S: a b a b c a b c a c b a b
P:           a b c a c


 以下是實現與測試的C代碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// BF (Brute Force) algorithm
// worst time complexity : O(m*n)
static int bf (const char*, const char*);
static int bf2(const char*, const char*);

int main(void)
{
    char* str = "ababcabcacbab";
    char* ptn = "abcac";

    printf("match1 at %d\n", bf(str, ptn));
    printf("match2 at %d\n", bf2(str, ptn));

    return 0;
}

int bf(const char* _str, const char* _ptn)
{
    int m, n, i, j;

    m = strlen(_str);
    n = strlen(_ptn);

    i = 0; j = 0;
    while(i<m && j<n)
    {
        if(_str[i] == _ptn[j])
        {
            printf("OK %d %d %c %c\n", i, j, _str[i], _ptn[j]);
            ++i;  ++j; 
        }
        else
        {
            printf("NO %d %d %c %c\n", i, j, _str[i], _ptn[j]);
            i = i-j+1; j = 0; 
        }
    }

    if(j >= n)
        return i-n;
    else
        return -1;
}



int bf2(const char* _str, const char* _ptn)
{
    int m, n, i, j;

    m = strlen(_str);
    n = strlen(_ptn);

    i = 0; j = 0;
    for(i=0; i<=(m-n); ++i)
    {
        for(j=0; j<n; ++j)
        {
            if(_str[i+j] != _ptn[j])
            {
                printf("NO %d %d %c %c\n", i+j, j, _str[i+j], _ptn[j]);
                break;
            }
            else
                printf("OK %d %d %c %c\n", i+j, j, _str[i+j], _ptn[j]);
        }
        if(n == j)  return i;
    }

    return -1;
}

 

BF算法的問題

BF算法在某些情況下存在效率上的問題。比如當S=‘aaaaaaabab’, P=‘aaab’時,BF算法匹配如下:

第1趟
S: a a a a a a a b a b
P: a a a b

第2趟
S: a a a a a a a b a b
P:   a a a b

第3趟
S: a a a a a a a b a b
P:     a a a b

第4趟
S: a a a a a a a b a b
P:       a a a b

第5趟
S: a a a a a a a b a b
P:         a a a b

若以i,j分別代表S串和P串當前比較的字符的位置/指針,那么(結合bf2函數))可以看出BF算法在每一趟匹配失敗后,i,j均要回退——j回退到0,i回退到i-j+1——再繼續下一趟比較(注意這里的i不是bf2函數里的i,而是相當於i+j)。而對以上特例來說,比如在第1趟比較后,S3!=P3事實上我們已經知道S1S2==‘aa’,因此不需要回退i,比較S1和P0, S2和P0,而只需回退j,比較S3和P2。這樣的話,由於i沒有回退,也就是減少了bf2中的外層循環次數,從而提高了匹配效率。如下圖所示,圖中↓表示當前指針i的位置。

         ↓
S: a a a a a a a b a b
P: a a a b       
        
->           
           ↓
S: a a a a a a a b a b
P:   a a a b                 
         ->             
             ↓
S: a a a a a a a b a b
P:     a a a b                       
         ->               
               ↓
S: a a a a a a a b a b
P:       a a a b       
         ->                 
                 ↓
S: a a a a a a a b a b
P:         a a a b

 

KMP算法

上面的改進算法就是KMP算法,它是由D.E.Knuth、J.H.Morris和V.R.Pratt同時發現的。KMP算法可以在O(m+n)的時間里完成串的模式匹配。它的主要思想是:每當一趟匹配過程中出現字符不匹配時,不需回退i指針,而是利用已經得到的“部分匹配”的結果將模式向右“滑動”盡可能遠的一段距離后,繼續匹配過程。

仍以一開始的例子S=‘ababcabcacbab’, P=‘abcac’來再看一遍KMP算法的匹配過程:

第1趟
            ↓ i=2
S: a b a b c a b c a c b a b
P: a b c 

第2趟
                         ↓ i=6
S: a b a b c a b c a c b a b

P:     a b c a c

第3趟 
                                    ↓ i=10
S: a b a b c a b c a c b a b
P:          (a)b c a c

第1趟正常比較,S2和P2不匹配,而且發現S1!=P0,因此,第2趟比較時,就不需要比較S1和P0了,i指針保持不動,只需將j指針向右滑動1個位置,直接比較S2和P1即可;

第2趟比較時,S6和P4不匹配,而且發現S3=='b',S4=='c',均與P0=='a'不匹配,因此,不需要進行以S3與P0、 S4和P0比較開始的這兩趟。另外,又現S5=='a'==P0,因此,也不需要回退i指針,只需將j指針向右滑動一個位置,直接比較P6與P1即可。與本文開頭的BF算法相比,KMP僅外層循環就減少為3趟,大大提升了匹配效率。

把上例的討論推廣到一般情況,設主串S=‘S0S1...Sm’,模式串P='P0P1...Pn',那么我們要解決的問題可表述為:當匹配過程中產生“失配”(即不相等)時,模式串“向右滑動”的距離應該是多少?或者說,當Si!=Pj時,Si應該和P中的哪個字符繼續比較?注意,主串的字符指針i不回退。

假設此時Si應和Pk(k<j)繼續比較,則k必滿足以下條件,且k是滿足此條件的最大值,即不存在k'(k<k'<j)也滿足此條件:

P0P1...Pk-1 == Si-kSi-k+1...Si-1               (1)

此時實際已得到的“部分匹配”結果是:

Pj-kPj-k+1...Pj-1 == Si-kSi-k+1...Si-1          (2)

由(1)、(2)兩式,可推得:

P0P1...Pk-1== Pj-kPj-k+1...Pj-1                          (3)

反過來講,如果在匹配過程中,有Si!=Pj,且有滿足(3)式的k(k<j)存在時,則i不動,只需繼續比較Si和Pk即可;如果k不存在,則繼續比較Si和P0。注意,k僅與模式P有關 ,而和主串S無關。

條件(3)表達了KMP算法的精髓之一。在主串指針i不移動的情況下,我們就是根據當前模式串指針j是否存在一個滿足條件(3)的k,來決定模式串“向右滑動”的距離:

a.如果存在這個k,也就是說,失配的Pj前存在一個長度為k(0<k<j)的子串,它與模式串P開頭的前k個字符組成的子串相同,或者叫“重疊”。而且,這個k是滿足此條件的“最大的”k,如果使用了可能的“較小的”k進行繼續比較,將會出現不必要的匹配過程。

b.如果k不存在,那么就從P0開始繼續比較。

要注意,如果存在k,就必須比較Pk與Si,不能比較P0與Si,否則將會出錯。比如當:

S: a a a a a a a b a b
P:       a a a b  

此時失配的i=6,j=3, 而k=2,(k需滿足0<k<j),下趟應比較S6與P2,如果無視k的存在,去比較S6與P0,就出錯了,找不到匹配:

S: a a a a a a a b a b
P:             a a a  

 

從(3)式及其附近的表述,我們已經知道k的值與主串無關,僅與模式串本身有關,因此,我們可以把k表示為模式串位置/指針j的函數next(j): 

next(j) = Max {k | 0<k<j,且P0P1...Pk-1== Pj-kPj-k+1...Pj-1 } ,k存在時
       或 = 0,k不存在時

舉個例子,當P=‘abaabcac’時,其各位置的next值計算過程為:

a  j=0, next(j)=0
b  j=1, 滿足0<k<1的整數k不存在,next(j)=0
a  j=2, 子串P1 != P0,k不存在,next(j)=0
a  j=3, 存在且僅存在子串P2==P0,next(j)=k=1
b  j=4, 存在且僅存在子串P3==P0,next(j)=k=1
c  j=5, 存在最大子串P3P4==P0P1,next(j)=k=2
a  j=6, 不存在要求子串,next(j)=0
c  j=7, 存在且僅存在子串P6==P0,next(j)=k=1

求得模式串的next函數后,KMP算法的匹配過程如下:

以指針i和j分別指示主串和模式串中當前要比較的字符,若在匹配過程中Si==Pj,則i和j分別增1;否則,i不變(不回退),而j回退到next(j),繼續進行比較:若匹配,則i,j分別增1,否則,j繼續回退到下一個next(j),如此類推。直到以下兩種可能:

  •  j回退到某個next(j)時 {next(next(...next(j)...)) },Si==Pj,則i,j分別增1;
  •  j退到0,即與模式串中第一個字符也不匹配,此時需要將i增1,j不變,即比較Si+1和Pj

KMP算法代碼如下:

 1 int kmp(const char* _str, const char* _ptn)
 2 {
 3     size_t m = strlen(_str);
 4     size_t n = strlen(_ptn);
 5     size_t i = 0, j = 0;
 6 
 7     size_t loop = 0;
 8     
 9     int* next = (int*)malloc(n*sizeof(int));
10     memset(next, 0, n*sizeof(int));
11     //kmp_next(_ptn, next);
12     kmp_next2(_ptn, next);
13 
14     for(i=0; i<n; ++i)
15         printf("%d ", next[i]);
16     printf("\n");
17 
18     i = 0; j = 0;
19     while(i < m && j < n)
20     {
21         loop++;
22         if(_str[i] == _ptn[j])
23             { ++i; ++j; }
24         else if(0 == j)
25             ++i;
26         else
27             j = next[j];
28     }
29 
30     free(next); next = NULL;
31 
32     printf("loop: %ld\n", loop);
33 
34     if(j >= n)
35         return i-n;
36     else
37         return -1;
38 }

 

求next函數值

求next(j)的一種方法是遞推法。

首先由定義可知next(0) = 0。若令next(j) = k,則模式P中存在下列關系:

P0P1...Pk-1== Pj-kPj-k+1...Pj-1   (1<k<j,且不存在k'>k滿足此條件)

此時我們需要遞推求得next(j+1)的值。分兩種情形:

  • 若Pk==Pj,則根據next函數定義,有next(j+1) == k+1 == next(j) + 1
  • 若pk!=Pj,這時可以應用KMP算法的思想,並把模式串本身既看成主串,又看成模式串,那么問題就轉化成一般的KMP算法問題。根據KMP算法,這時,應把第next(k)個字符與Pj進行比較。若Pj==Pnext(k) ,則next(j+1) == next(k)+1;若Pj != Pnext(k),則繼續比較Pj和Pnext(next(k)) ......依此類推,直至Pj和某個字符匹配成功。或者不存在任何k'(1<k'<k<j)可以匹配成功,則令next(j+1)=0。

求next函數值的代碼如下:

 1 void kmp_next(const char* _ptn, int* _next)
 2 {
 3     size_t n = strlen(_ptn);
 4     size_t i, j;
 5 
 6     if(n >= 1)
 7         _next[0] = 0;
 8 
 9     i = 1; j = 0;
10     while(i < n)
11     {
12         if(_ptn[i] == _ptn[j])
13             _next[++i] = ++j;
14         else if(0 == j)
15             _next[++i] = j;
16         else
17             j = _next[j];
18     }
19 }

 

next函數的改進

以上的next函數值求法對於某些情況會有一些不足。比如當S=‘aaabaaaab’,P=‘aaaab’時,進行到以下匹配:

S: a a a b a a a a b
P: a a a a b

發生失配S3!=P3。此時,如果按之前的next函數值求法,會得到next(1)==0, next(2)==1, next(3)==2,那么根據KMP算法,S3會與Pnext(3)即P2進行比較;發現不匹配,那么模式串各右滑動,S3繼續與Pnext(2)即P1進行比較;發現還不匹配,繼續向右滑動模式串,S3繼續與Pnext(1)即P0進行比較,發現仍不匹配,沒辦法,才會將S串上的i指針增1,讓S4與P0比較。

然而,由於P0,P1,P2和P3相等,既然S3!=P3,那么根本沒必要進行接下來的比較了,i指針可以直接增1,進行下邊的比較。推廣到一般情況來說,就是如果這樣的情況發生,我們要人為地讓j的回退幅度更大,以減少不必要的比較。 根據這個發現,我們可以對next函數的遞推求值算法的情形1進行優化:

若Pk==Pnext(j)==Pj時,恰滿足Pk+1==Pnext(j)+1==Pj+1,那么當Pj+1失配時,不需要比較Pnext(j)+1和Pj+1,即next(j+1)不用取next(j)+1,而可以將模式串中要比較的位置再向左移,到next(j+1) = next(next(j+1))。

 正如上圖所示,由於Pj+1==Pk+1,next(j+1)的值可以跨過k+1,而直接到k'==next(k+1),即next(j+1) = next(k+1) == next(next(j+1))。

 改進后的next函數代碼如下:

 1 void kmp_next2(const char* _ptn, int* _next)
 2 {
 3     size_t n = strlen(_ptn);
 4     size_t i, j;
 5     
 6     if(n >= 1)
 7         _next[0] = 0;
 8     
 9     i = 1; j = 0;
10     while(i < n)
11     {
12         if(_ptn[i] == _ptn[j])
13         {
14             ++i; ++j;
15             if(_ptn[i] != _ptn[j])
16                 _next[i] = j;
17             else
18                 _next[i] = _next[j];
19         }
20         else if(0 == j)
21             _next[++i] = j;
22         else
23             j = _next[j];
24     }
25 }

 

【參考資料】

《數據結構(C語言版)》,嚴蔚敏 吳偉民 編著,清華大學出版社。

 


免責聲明!

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



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