1. 程式人生 > >c/c++程式之_KMP字串模式匹配詳解(非常不錯的詳解)

c/c++程式之_KMP字串模式匹配詳解(非常不錯的詳解)

  KMP字串模式匹配通俗點說就是一種在一個字串中定位另一個串的高效演算法。簡單匹配演算法的時間複雜度為O(m*n);KMP匹配演算法。可以證明它的時間複雜度為O(m+n).

一.  簡單匹配演算法

先來看一個簡單匹配演算法的函式:

int Index_BF ( char S [ ], char T [ ], int pos )
{
    /* 若串 S 中從第pos(S 的下標0≤pos<StrLength(S))個字元
    起存在和串 T 相同的子串,則稱匹配成功,返回第一個
    這樣的子串在串 S 中的下標,否則返回 -1    */
    int i = pos, j = 0;
    while ( S[i+j] != '/0'&& T[j] != '/0')
        if ( S[i+j] == T[j] )
            j ++; // 繼續比較後一字元
        else
        {
            i ++;
            j = 0; // 重新開始新的一輪匹配
        }
    if ( T[j] == '/0')
        return i; // 匹配成功   返回下標
    else
        return -1; // 串S中(第pos個字元起)不存在和串T相同的子串
} // Index_BF

   此演算法的思想是直截了當的:將主串S中某個位置i起始的子串和模式串T相比較。即從 j=0 起比較 S[i+j]  T[j],若相等,則在主串 S 中存在以 i 為起始位置匹配成功的可能性,繼續往後比較( j逐步增1 ),直至與T串中最後一個字元相等為止,否則改從S串的下一個字元起重新開始進行下一輪的"匹配",即將串T向後滑動一位,即 i 1,而 j 退回至0,重新開始新一輪的匹配。

       例如:在串S=”abcabcabdabba”中查詢T=” abcabd”(我們可以假設從下標0開始):先是比較S[0]T[0]是否相等,然後比較S[1] T[1]是否相等我們發現一直比較到S[5] 

T[5]才不等。如圖:

不知為什麼,圖沒顯示

當這樣一個失配發生時,T下標必須回溯到開始,S下標回溯的長度與T相同,然後S下標增1,然後再次比較。如圖:

這次立刻發生了失配,T下標又回溯到開始,S下標增1,然後再次比較。如圖:

    又一次發生了失配,所以T下標又回溯到開始,S下標增1,然後再次比較。這次T中的所有字元都和S中相應的字元匹配了。函式返回TS中的起始下標3。如圖:

. KMP匹配演算法

還是相同的例子,在S=”abcabcabdabba”中查詢T=”abcabd”如果使用KMP匹配演算法,當第一次搜尋到S[5] T[5]不等後,S下標不是回溯到1T下標也不是回溯到開始,而是根據T

T[5]==’d’的模式函式值(next[5]=2,為什麼?後面講),直接比較S[5] T[2]是否相等,因為相等,ST的下標同時增加;因為又相等,ST的下標又同時增加。。。最終在S中找到了T如圖:

 KMP匹配演算法和簡單匹配演算法效率比較,一個極端的例子是:

       在S=AAAAAA…AAB(100A)中查詢T=”AAAAAAAAAB”, 簡單匹配演算法每次都是比較到T的結尾,發現字元不同,然後T的下標回溯到開始,S的下標也要回溯相同長度後增1,繼續比較。如果使用KMP匹配演算法,就不必回溯.

對於一般文稿中串的匹配,簡單匹配演算法的時間複雜度可降為O (m+n),因此在多數的實際應用場合下被應用。

 KMP演算法的核心思想是利用已經得到的部分匹配資訊來進行後面的匹配過程。看前面的例子。為什麼T[5]==’d’的模式函式值等於2next[5]=2),其實這個2表示T[5]==’d’的前面有2個字元和開始的兩個字元相同,且T[5]==’d’不等於開始的兩個字元之後的第三個字元(T[2]=’c’.如圖:

也就是說,如果開始的兩個字元之後的第三個字元也為’d’,那麼,儘管T[5]==’d’的前面有2個字元和開始的兩個字元相同,T[5]==’d’的模式函式值也不為2,而是為0

前面我說:在S=”abcabcabdabba”中查詢T=”abcabd”,如果使用KMP匹配演算法,當第一次搜尋到S[5] T[5]不等後,S下標不是回溯到1T下標也不是回溯到開始,而是根據TT[5]==’d’的模式函式值,直接比較S[5] T[2]是否相等。。。為什麼可以這樣?

       剛才我又說:“(next[5]=2),其實這個2表示T[5]==’d’的前面有2個字元和開始的兩個字元相同”。請看圖:因為,S[4] ==T[4]S[3] ==T[3],根據next[5]=2,有T[3]==T[0]T[4] ==T[1],所以S[3]==T[0]S[4] ==T[1](兩對相當於間接比較過了),因此,接下來比較S[5] T[2]是否相等。。。

  有人可能會問:S[3]T[0]S[4] T[1]是根據next[5]=2間接比較相等,那S[1]T[0]S[2] T[0]之間又是怎麼跳過,可以不比較呢?因為S[0]=T[0]S[1]=T[1]S[2]=T[2],而T[0]  !=  T[1], T[1] !=  T[2],==>  S[0]  != S[1],S[1] != S[2],所以S[1]  != T[0],S[2] != T[0].  還是從理論上間接比較了。

       有人疑問又來了,你分析的是不是特殊輕況啊。

       假設S不變,在S中搜索T=abaabd”呢?答:這種情況,當比較到S[2]T[2]時,發現不等,就去看next[2]的值,next[2]=-1,意思是S[2]已經和T[0] 間接比較過了,不相等,接下來去比較S[3]T[0]吧。

       假設S不變,在S中搜索T=abbabd”呢?答:這種情況,當比較到S[2]T[2]時,發現不等,就去看next[2]的值,next[2]=0,意思是S[2]已經和T[2]比較過了,不相等,接下來去比較S[2]T[0]吧。

       假設S=”abaabcabdabba”S中搜索T=abaabd”呢?答:這種情況當比較到S[5]T[5]時,發現不等,就去看next[5]的值,next[5]=2,意思是前面的比較過了,其中,S[5]的前面有兩個字元和T的開始兩個相等,接下來去比較S[5]T[2]吧。

總之,有了串的next值,一切搞定。那麼,怎麼求串的模式函式值next[n]呢?(本文中next值、模式函式值、模式值是一個意思。)

怎麼求串的模式值next[n]

定義

1)next[0] = -1   意義:任何串的第一個字元的模式值規定為 -1

2next[j] = -1    意義:模式串T中下標為j的字元,如果與首字元

         相同,且j的前面的1—k個字元與開頭的1—k

         個字元不等(或者相等但T[k] == T[j])(≤ k < j)。

         如:T = ”abCabCad”  next[6] = -1,因T[3] = T[6]

3next[j] = k     意義: 模式串T中下標為j的字元,如果j的前面k

         字元與開頭的k個字元相等,且T[j] != T[k] ≤ k < j)。

         即T[0]T[1]T[2]。。。T[k-1] == T[j-k]T[j-k+1]T[j-k+2]…T[j-1] T[j] != T[k].≤ k < j;

(4)next[j] = 0     意義: 除(1)(2)(3)的其他情況。

舉例

01)T=abcac”的模式函式的值。

        next[0] = -1  根據(1

        next[1] = 0   根據 (4)   因(3)有1 <= k < j;不能說,j=1,T[j-1] == T[0]

        next[2] = 0   根據 (4)   因(3)有1 <= k < j;T[0] = a!=T[1] = b

        next[3] = -1  根據 (2)

        next[4] = 1   根據 (3)  T[0] == T[3]  T[1] != T[4]

下標

0

1

2

3

4

T

a

b

c

a

c

next

-1

0

0

-1

1

       若T=abcab”將是這樣:

下標

0

1

2

3

4

T

a

b

c

a

b

next

-1

0

0

-1

0

   為什麼T[0]==T[3],還會有next[4]=0因為T[1]==T[4], 根據 (3)” T[j] != T[k]”被劃入(4)。

02)來個複雜點的,求T=”ababcaabc” 的模式函式的值。

next[0] = -1   根據(1

next[1] = 0    根據 (4)

next[2] = -1   根據 (2)

next[3] = 0   根據 (3) T[0] = T[2] T[1] = T[3] 被劃入(4

next[4] = 2   根據 (3) T[0]T[1] = T[2]T[3] T[2] != T[4]

next[5] = -1  根據 (2) 

next[6] = 1   根據 (3) T[0] = T[5] T[1] != T[6]

next[7] = 0   根據 (3) T[0] = T[6] T[1] = T[7] 被劃入(4

next[8] = 2   根據 (3) T[0]T[1] = T[6]T[7] T[2] != T[8]

下標

0

1

2

3

4

5

6

7

8

T

a

b

a

b

c

a

a

b

c

next

-1

0

-1

0

2

-1

1

0

2

只要理解了next[3] = 0,而不是 = 1next[6] = 1,而不是 = -1next[8] =2,而不是= 0,其他的好象都容易理解。

03)   來個特殊的,求 T = ”abCabCad” 的模式函式的值。

下標

0

1

2

3

4

5

6

7

T

a

b

C

a

b

C

a

d

next

-1

0

0

-1

0

0

-1

4

next[5] = 0  根據 (3) T[0]T[1] = T[3]T[4],T[2] == T[5]

next[6] = -1  根據 (2) 雖前面有abC = abC,T[3] == T[6]

next[7] =4   根據 (3) 前面有abCa = abCa, T[4] != T[7]

T[4] == T[7],即T=” adCadCad”,那麼將是這樣:next[7] = 0, 而不是 = 4,因為T[4] == T[7].

下標

0

1

2

3

4

5

6

7

T

a

d

C

a

d

C

a

d

next

-1

0

0

-1

0

0

-1

0

如果你覺得有點懂了,那麼

練習:求T=”AAAAAAAAAAB” 的模式函式值,並用後面的求模式函式值函式驗證。

意義

        next 函式值究竟是什麼含義,前面說過一些,這裡總結。

設在字串S中查詢模式串T,若S[m]!=T[n],那麼,取T[n]的模式函式值next[n],

1.       next[n] =  -1 表示S[m]T[0]間接比較過了,不相等,下一次比較 S[m+1] T[0]

2.       next[n] = 0 表示比較過程中產生了不相等,下一次比較 S[m] T[0]

3.       next[n] = k >0 但k<n, 表示,S[m]的前k個字元與T中的開始k個字元已經間接比較相等了,下一次比較S[m]T[k]相等嗎?

4.       其他值,不可能。

求串T的模式值next[n]的函式

       說了這麼多,是不是覺得求串T的模式值next[n]很複雜呢?要叫我寫個函數出來,目前來說,我寧願去登天。好在有現成的函式,當初發明KMP演算法,寫出這個函式的先輩,令我佩服得六體投地。我等後生小子,理解起來,都要反覆琢磨。下面是這個函式:

void get_nextval(const char *T, int next[])
{
    // 求模式串T的next函式值並存入陣列 next。
    int j = 0, k = -1;
    next[0] = -1;
    while ( T[j/*+1*/] != '/0' )
    {
        if (k == -1 || T[j] == T[k])
        {
            ++j;
            ++k;
            if (T[j]!=T[k])
                next[j] = k;
            else
                next[j] = next[k];
        }// if
        else
            k = next[k];
    }// while
    ////這裡是我加的顯示部分
    // for(int  i=0;i<j;i++)
    //{
    //     cout<<next[i];
    //}
    //cout<<endl;
}// get_nextval 


另一種寫法,也差不多。
void getNext(const char* pattern,int next[])
{
    next[0]=   -1;
    int k=-1,j=0;
    if(k!=  -1  &&  pattern[
                while(pattern[j]  !=  '/0')
{

    k]!=  pattern[j] )
    k=next[k];
    ++j;
    ++k;
    else
        next[j]=k;

        if(pattern[k]==  pattern[j])
            next[j]=next[k];

        }
////這裡是我加的顯示部分
// for(int  i=0;i<j;i++)
//{
//     cout<<next[i];
//}
//cout<<endl;
}


下面是KMP模式匹配程式,各位可以用他驗證。記得加入上面的函式
#include <iostream.h>
#include <string.h>
int KMP(const char *Text,const char* Pattern) //const 表示函式內部不會改變這個引數的值。
{
    if( !Text||!Pattern||  Pattern[0]=='\0'  ||  Text[0]=='\0' )//
        return -1;//空指標或空串,返回-1。
    int len=0;
    const char * c=Pattern;
    while(*c++!='\0')//移動指標比移動下標快。
    {
        ++len;//字串長度。
    }
    int *next=new int[len+1];
    get_nextval(Pattern,next);//求Pattern的next函式值

    int index=0,i=0,j=0;
    while(Text[i]!='\0'  && Pattern[j]!='\0' )
    {
        if(Text[i]== Pattern[j])
        {
            ++i;// 繼續比較後繼字元
            ++j;
        }
        else
        {
            index += j-next[j];
            if(next[j]!=-1)
                j=next[j];// 模式串向右移動
            else
            {
                j=0;
                ++i;
            }
        }
    }//while

    delete []next;
    if(Pattern[j]=='\0')
        return index;// 匹配成功
    else
        return -1;
}
int main()//abCabCad
{
    char* text="bababCabCadcaabcaababcbaaaabaaacababcaabc";
    char*pattern="adCadCad";
    //getNext(pattern,n);
    //get_nextval(pattern,n);
    cout<<KMP(text,pattern)<<endl;
    return 0;
}


五.其他表示模式值的方法

上面那種串的模式值表示方法是最優秀的表示方法,從串的模式值我們可以得到很多資訊,以下稱為第一種表示方法。第二種表示方法,雖然也定義next[0]= -1,但後面絕不會出現 -1,除了next[0]其他模式值next[j]=k(0k<j)的意義可以簡單看成是:下標為j的字元的前面最多k個字元與開始的k個字元相同,這裡並不要求T[j] != T[k]其實next[0]也可以定義為0(後面給出的求串的模式值的函式和串的模式匹配的函式,是next[0]=0的),這樣,next[j]=k(0k<j)的意義都可以簡單看成是:下標為j的字元的前面最多k個字元與開始的k個字元相同。第三種表示方法是第一種表示方法的變形,即按第一種方法得到的模式值,每個值分別加1,就得到第三種表示方法。第三種表示方法,我是從論壇上看到的,沒看到詳細解釋,我估計是為那些這樣的程式語言準備的:陣列的下標從1開始而不是0

下面給出幾種方法的例子:

表一。

下標

0

1

2

3

4

5

6

7

8

T

a

b

a

b

c

a

a

b

c

(1) next

-1

0

-1

0

2

-1

1

0

2

(2) next

-1

0

0

1

2

0

1

1

2

(3) next

0

1

0

1

3

0

2

1

3

第三種表示方法,在我看來,意義不是那麼明瞭,不再討論。

      表二。

下標

0

1

2

3

4

T

a

b

c

a

c

(1)next

-1

0

0

-1

1

(2)next

-1

0

0

0

1

 表三。