1. 程式人生 > IOS開發 >資料結構與演演算法-KMP模式匹配演演算法

資料結構與演演算法-KMP模式匹配演演算法

KMP模式匹配演演算法原理

如果主串S="abcdefgab",其實還可以更長一些,我們就省略掉只保留前9位,我們要匹配的T="abcdex",那麼如果用BF演演算法的話,前5個字母,兩個串完全相等,直到第6個字母,“f”與“x”不等,如圖5-7-1的①所示。

image-20200502151505041

接下來,按照BF演演算法,應該是如上圖的流程②③④⑤⑥。即主串S中當i=2、3、4、5、6時,首字元與子串T的首字元均不等。

似乎這也是理所當然,原來的演演算法就是這樣設計的。可仔細觀察發現。對於要匹配的子串T來說,“abcdex”首字任意一個字元都不相等。也就是說,既然“a”不與自己後面的子串中任何一字元相等,那麼對於上圖的①來說,前五位字元分別相等,意味著子串T的首字元“a”不可能與S串的第2位到第5位的字元相等。在上圖中,②③④⑤的判斷都是多餘。

同樣道理,在我們知道T串中首字元“a”與T中後面的字元均不相等的前提下,T串的“a”與S串後面的“c”、“d”、“e”也都可以在①之後就可以確定是不相等的,所以這個演演算法當中②③④⑤沒有必要,只保留①⑥即可,如下圖所示。

image-20200502151744770

之所以保留⑥中的判斷是因為在①中T[6]≠S[6],儘管我們已經知道T[1]≠T[6],但也不能斷定T[1]一定不等於S[6],因此需要保留⑥這一步。”

如果T串後面也含有首字元“a”的字元怎麼辦呢?

我們來看下面一個例子,假設S="abcababca",T="abcabx"。對於開始的判斷,前5個字元完全相等,第6個字元不等,如下圖的①。此時,根據剛才的經驗,T的首字元“a”與T的第二位字元“b”、第三位字元“c”均不等,所以不需要做判斷,下圖的BF演演算法步驟②③都是多餘。

image-20200502152540621

因為T的首位“a”與T第四位的“a”相等,第二位的“b”與第五位的“b”相等。而在①時,第四位的“a”與第五位的“b”已經與主串S中的相應位置比較過了,是相等的,因此可以斷定,T的首字元“a”、第二位的字元“b”與S的第四位字元和第五位字元也不需要比較了,肯定也是相等的——之前比較過了,還判斷什麼,所以④⑤這兩個比較得出字元相等的步驟也可以省略。

也就是說,對於在子串中有與首字元相等的字元,也是可以省略一部分不必要的判斷步驟。如下圖所示,省略掉右圖的T串前兩位“a”與“b”同S串中的4、5位置字元匹配操作。

image-20200502154228691

對比這兩個例子,我們會發現在①時,我們的i值,也就是主串當前位置的下標是6,②③④⑤,i值是2、3、4、5,到了⑥,i值才又回到了6。即我們在BF演演算法中,主串的i值是不斷地回溯來完成的。而我們的分析發現,這種回溯其實是可以不需要的——正所謂好馬不吃回頭草,我們的KMP模式匹配演演算法就是為了讓這沒必要的回溯不發生。

既然i值不回溯,也就是不可以變小,那麼要考慮的變化就是j值了。通過觀察也可發現,我們屢屢提到了T串的首字元與自身後面字元的比較,發現如果有相等字元,j值的變化就會不相同。也就是說,這個j值的變化與主串其實沒什麼關係,關鍵就取決於T串的結構中是否有重複的問題。

由於T="abcdex",當中沒有任何重複的字元,所以j就由6變成了1。由於T="abcabx",字首的“ab”與最後“x”之前串的字尾“ab”是相等的。因此j就由6變成了3。因此,我們可以得出規律,j值的多少取決於當前字元之前的串的前字尾的相似度。

我們把T串各個位置的j值的變化定義為一個陣列next,那麼next的長度就是T串的長度。於是我們可以得到下面的函式定義:

image-20200502155333259

next陣列值推導

“具體如何推匯出一個串的next陣列值呢,我們來看一些例子。

1.T="abcdex

j 123456
模式串T abcdex
next[j] 011111

1)當j=1時,next[1]=0;

2)當j=2時,j由1到j-1就只有字元“a”,屬於其他情況next[2]=1;

3)當j=3時,j由1到j-1串是“ab”,顯然“a”與“b”不相等,屬其他情況,next[3]=1;

4)以後同理,所以最終此T串的next[j]為011111。

2.T="abcabx"

j 123456
模式串T abcabx
next[j] 011123

1)當j=1時,next[1]=0;

2)當j=2時,同上例說明,next[2]=1;

3)當j=3時,同上,next[3]=1;

4)當j=4時,同上,next[4]=1;

5)當j=5時,此時j由1到j-1的串是“abca”,字首字元“a”與字尾字元“a”相等(字首用下劃線表示,字尾用斜體表示),因此可推算出k值為2(由‘p1...pk-1’=‘pj-k+1...pj-1’,得到p1=p4)因此next[5]=2;

6)當j=6時,j由1到j-1的串是“abcab”,由於字首字元“ab”與字尾“ab”相等,所以next[6]=3。

我們可以根據經驗得到如果前字尾一個字元相等,k值是2,兩個字元k值是3,n個相等k值就是n+1。

3.T="ababaaaba"

j 123456789
模式串T ababaaaba
next[j] 011234223

1)當j=1時,next[1]=0;

2)當j=2時,同上next[2]=1;

3)當j=3時,同上next[3]=1;

4)當j=4時,j由1到j-1的串是“aba”,字首字元“a”與字尾字元“a”相等,next[4]=2;

5)當j=5時,j由1到j-1的串是“abab”,由於字首字元“ab”與字尾“ab”相等,所以next[5]=3;

6)當j=6時,j由1到j-1的串是“ababa”,由於字首字元“aba”與字尾“aba”相等,所以next[6]=4;

7)當j=7時,j由1到j-1的串是“ababaa”,由於字首字元“ab”與字尾“aa”並不相等,只有“a”相等,所以next[7]=2;

8)當j=8時,j由1到j-1的串是“ababaaa”,只有“a”相等,所以next[8]=2;

9)當j=9時,j由1到j-1的串是“ababaaab”,由於字首字元“ab”與字尾“ab”相等,所以next[9]=3。

4.T="aaaaaaaab"

j 123456789
模式串T aaaaaaaab
next[j] 012345678

1)當j=1時,next[1]=0;

2)當j=2時,同上next[2]=1;

3)當j=3時,j由1到j-1的串是“aa”,字首字元“a”與字尾字元“a”相等,next[3]=2;

4)當j=4時,j由1到j-1的串是“aaa”,由於字首字元“aa”與字尾“aa”相等,所以next[4]=3;

5)……

6)當j=9時,j由1到j-1的串是“aaaaaaaa”,由於字首字元“aaaaaaa”與字尾“aaaaaaa”相等,所以next[9]=8。

KMP模式匹配演演算法實現

next陣列計算實現

/* 通過計算返回子串T的next陣列。 */
void get_next(String T,int *next)
{
    int i,j;
    i = 1;
    j = 0;
    next[1] = 0;
    /* 此處T[0]表示串T的長度 */
    while (i < T[0])
    {
        /* T[i]表示字尾的單個字元, */
        /* T[j]表示字首的單個字元 */
        if (j == 0 || T[i] == T[j])
        {
            ++i;
            ++j;
            next[i] = j;
        }
        else
            /* 若字元不相同,則j值回溯 */
            j = next[j];
    }
}
複製程式碼

這段程式碼的目的就是為了計算出當前要匹配的串T的next陣列。

/* 返回子串T在主串S中第pos個字元之後的位置。
   若不存在,則函式返回值為0。 */
/* T非空,1≤pos≤StrLength(S)。 */
int Index_KMP(String S,String T,int pos)
{
    /* i用於主串S當前位置下標值,若pos不為1, */
    /* 則從pos位置開始匹配 */
    int i = pos;
    /* j用於子串T中當前位置下標值 */
    int j = 1;
    /* 定義一next陣列 */
    int next[255];
    /* 對串T作分析,得到next陣列 */
    get_next(T,next);
    /* 若i小於S的長度且j小於T的長度時, */
    /* 迴圈繼續 */
    while (i <= S[0] && j <= T[0])
    {
        /* 兩字母相等則繼續,相對於BF演演算法增加了 */
        /* j=0判斷 */
        if (j == 0 || S[i] == T[j])
        {
            ++i;
            ++j;
        }
        /* 指標後退重新開始匹配 */
        else
        {
            /* j退回合適的位置,i值不變 */
            j = next[j];
        }
    }
    if (j > T[0])
        return i - T[0];
    else
        return 0;
}
複製程式碼

KMP模式匹配演演算法改進

KMP還是有缺陷的。比如,如果我們的主串S="aaaabcde",子串T="aaaaax",其next陣列值分別為012345,在開始時,當i=5、j=5時,我們發現“b”與“a”不相等,如下圖的①,因此j=next[5]=4,如圖中的②,此時“b”與第4位置的“a”依然不等,j=next[4]=3,如圖中的③,後依次是④⑤,直到j=next[1]=0時,根據演演算法,此時i++、j++,得到i=6、j=1,如圖中的⑥。

image-20200502164828005

我們發現,當中的②③④⑤步驟,其實是多餘的判斷。由於T串的第二、三、四、五位置的字元都與首位的“a”相等,那麼可以用首位next[1]的值去取代與它相等的字元後續next[j]的值,這是個很好的辦法。因此我們對求next函式進行了改良。

假設取代的陣列為nextval,程式碼如下:

/* 求模式串T的next函式修正值並存入陣列
nextval */
void get_nextval(String T,int *nextval)
{
    int i,j;
    i = 1;
    j = 0;
    nextval[1] = 0;
    /* 此處T[0]表示串T的長度 */
    while (i < T[0])                        
    {
        /* T[i]表示字尾的單個字元, */
        /* T[j]表示字首的單個字元 */
        if (j == 0 || T[i] == T[j])         
        {
            ++i;
            ++j;
            /* 若當前字元與字首字元不同 */
            if (T[i] != T[j])               
                /* 則當前的j為nextval在i位置的值 */
                nextval[i] = j;             
            else
                /* 如果與字首字元相同,則將字首 */
                /* 字元的nextval值賦值給nextval在i位置的值 */
                nextval[i] = nextval[j];    
        }
        else
            /* 若字元不相同,則j值回溯 */
            j = nextval[j];                 
    }
}
複製程式碼

nextval陣列值推導

改良後,我們之前的例子nextval值就與next值不完全相同了。比如:

1.T="ababaaaba"

j 123456789
模式串T ababaaaba
next[j] 011234223
nextval[j] 010104210

先算出next陣列的值分別為011234223,然後再分別判斷。

1)當j=1時,nextval[1]=0;

2)當j=2時,因第二位字元“b”的next值是1,而第一位就是“a”,它們不相等,所以nextval[2]=next[2]=1,維持原值。

3)當j=3時,因為第三位字元“a”的next值為1,所以與第一位的“a”比較得知它們相等,所以nextval[3]=nextval[1]=0;

image-20200502165634122

4)當j=4時,第四位的字元“b”next值為2,所以與第二位的“b”相比較得到結果是相等,因此nextval[4]=nextval[2]=1;如下圖所示。

image-20200502165729068

5)當j=5時,next值為3,第五個字元“a”與第三個字元“a”相等,因此nextval[5]=nextval[3]=0;

6)當j=6時,next值為4,第六個字元“a”與第四個字元“b”不相等,因此nextval[6]=4;

7)當j=7時,next值為2,第七個字元“a”與第二個字元“b”不相等,因此nextval[7]=2;

8)當j=8時,next值為2,第八個字元“b”與第二個字元“b”相等,因此nextval[8]=nextval[2]=1;

9)當j=9時,next值為3,第九個字元“a”與第三個字元“a”相等,因此nextval[9]=nextval[3]=0。

2.T="aaaaaaaab"

j 123456789
模式串T aaaaaaaab
next[j] 012345678
nextval[j] 000000008

“先算出next陣列的值分別為012345678,然後再分別判斷。

1)當j=1時,nextval[1]=0;

2)當j=2時,next值為1,第二個字元與第一個字元相等,所以nextval[2]=nextval[1]=0;

3)同樣的道理,其後都為0……;

4)當j=9時,next值為8,第九個字元“b”與第八個字元“a”不相等,所以nextval[9]=8。

總結

改進過的KMP演演算法,它是在計算出next值的同時,如果a位字元與它next值指向的b位字元相等,則該a位的nextval就指向b位的nextval值,如果不等,則該a位的nextval值就是它自己a位的next的值。”