資料結構與演演算法-KMP模式匹配演演算法
KMP模式匹配演演算法原理
如果主串S="abcdefgab",其實還可以更長一些,我們就省略掉只保留前9位,我們要匹配的T="abcdex",那麼如果用BF演演算法的話,前5個字母,兩個串完全相等,直到第6個字母,“f”與“x”不等,如圖5-7-1的①所示。
接下來,按照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”也都可以在①之後就可以確定是不相等的,所以這個演演算法當中②③④⑤沒有必要,只保留①⑥即可,如下圖所示。
之所以保留⑥中的判斷是因為在①中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演演算法步驟②③都是多餘。
因為T的首位“a”與T第四位的“a”相等,第二位的“b”與第五位的“b”相等。而在①時,第四位的“a”與第五位的“b”已經與主串S中的相應位置比較過了,是相等的,因此可以斷定,T的首字元“a”、第二位的字元“b”與S的第四位字元和第五位字元也不需要比較了,肯定也是相等的——之前比較過了,還判斷什麼,所以④⑤這兩個比較得出字元相等的步驟也可以省略。
也就是說,對於在子串中有與首字元相等的字元,也是可以省略一部分不必要的判斷步驟。如下圖所示,省略掉右圖的T串前兩位“a”與“b”同S串中的4、5位置字元匹配操作。
對比這兩個例子,我們會發現在①時,我們的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串的長度。於是我們可以得到下面的函式定義:
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,如圖中的⑥。
我們發現,當中的②③④⑤步驟,其實是多餘的判斷。由於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;
4)當j=4時,第四位的字元“b”next值為2,所以與第二位的“b”相比較得到結果是相等,因此nextval[4]=nextval[2]=1;如下圖所示。
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的值。”