kmp演算法原理及實現
字串匹配問題:其主要功能給定兩個字串T和f(字串),長度分別為n和m,判斷f是否在T中出現,如果出現則返回出現的位置。給定兩個字串T和f(字串),長度分別為n和m,判斷f是否在T中出現,如果出現則返回出現的位置。如:有一個字串"BBC ABCDAB ABCDABCDABDE",我想知道,裡面是否包含另一個字串"ABCDABD"?
樸素字串匹配演算法:遍歷T的每一個位置,然後從該位置開始和f進行匹配,但是這種方法的複雜度是O(nm)。這種演算法沒有利用匹配過的資訊,對於字串來說每次都從頭開始比較,而對於主串來說,經常需要重複比較之前已經檢測過的字元,因而速度很慢。模式匹配是一個常見的應用問題,用的廣了,就有人想法去優化了。Rabin-Karp演算法、有限自動機等等,前仆後繼,最終出現了KMP(Knuth-Morris-Pratt)演算法。
kmp演算法被稱為“看毛片”演算法,是一個效率非常高的字串匹配演算法。
kmp演算法通過一個O(m)的預處理來構建一個字串f的字首陣列(即計算字串f每一個位置的字串的字首和字尾公共部分的最大長度,不包括字串本身,否則最大長度始終是字串本身),接下來的匹配過程會不斷地使用到該字首陣列(而且對於主串T只需遍歷一次),使匹配的複雜度降為O(n+m)。
構建字首陣列:
規定:next[0]=next[1]=0;
next[i]就是字首陣列,下面通過1個例子來看如何構造字首陣列。
例子1:cacca有5個字首,求出其對應的next陣列。
字首2為ca,顯然首尾沒有相同的字元,next[2] = 0
字首3為cac,顯然首尾有共同的字元c,故next[3] = 1
字首4為cacc,首尾有共同的字元c,故next[4] = 1
字首5為cacca,首尾有共同的字元ca,故next[5] = 2
在本例中對於字串"ABCDABD"的字首陣列如下:
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
f(i) | A | B | C | D | A | B | D |
next[i] | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
匹配過程:
1.
首先,字串"BBC ABCDAB ABCDABCDABDE"的第一個字元與搜尋詞"ABCDABD"的第一個字元,進行比較。因為B與A不匹配,所以搜尋詞後移一位。
2.
因為B與A不匹配,搜尋詞再往後移。
3.
就這樣,直到字串有一個字元,與搜尋詞的第一個字元相同為止。
4.
接著比較字串和搜尋詞的下一個字元,還是相同。
5.
直到字串有一個字元,與搜尋詞對應的字元不相同為止。
6.
樸素字串匹配過程:將搜尋詞整個後移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把"搜尋位置"移到已經比較過的位置,重比一遍。
而一個基本事實是,當空格與D不匹配時,你其實知道前面六個字元是"ABCDAB"。
KMP演算法的想法:設法利用這個已知資訊,不要把"搜尋位置"移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。而這時候就要利用到上面所產 生的字首陣列了,查表可知前面六個字元是"ABCDAB"的next[6]=2;因而直接將字串匹配位置移動6-2=4個位置;
即如下圖:
移動位置=已匹配的字元數 - 對應的部分匹配值
7.
因為空格與C不匹配,搜尋詞還要繼續往後移。這時,已匹配的字元數為2("AB"),對應的"部分匹配值"為0。所以,移動位數 = 2 - 0,結果 為 2,於是將搜尋詞向後移2位。
8.
因為空格與A不匹配,繼續後移一位。
9.
逐位比較,直到發現C與D不匹配。於是,移動位數 = 6 - 2,繼續將搜尋詞向後移動4位。
10.
逐位比較,直到搜尋詞的最後一位,發現完全匹配,於是搜尋完成。如果還要繼續搜尋(即找出全部匹配),移動位數 = 7 - 0,再將搜尋詞向後 移動7位,這裡就不再重複了。
實現如下:
#include "iostream"
using namespace std;
void compute_prefix(int *next, char *p)
{
int i, n, k;
n = strlen(p);
next[1] = next[0] = 0;
k = 0; /* 第i次迭代開始之前,k表示next[i-1]的值 */
for (i = 2; i <= n; i++) {
for (; k != 0 && p[k] != p[i - 1]; k = next[k]);
if (p[k] == p[i - 1]) k++;
next[i] = k;
}
}
void kmp_match(char *text, char *p, int *next)
{
int m, n, s, q;
m = strlen(p);
n = strlen(text);
q = s = 0; /* q表示上一次迭代匹配了多少個字元,
s表示這次迭代從text的哪個字元開始比較 */
while (s < n) {
for (q = next[q]; q < m && p[q] == text[s]; q++, s++);
if (q == 0) s++;
else if (q == m) {
printf("pattern occurs with shift %d\n", s - m);
}
}
}
int main()
{
int next[101], n;
char *p = "ababababca";
char *text = "ababababcadababababcadababababcadababababca";
compute_prefix(next, p);
kmp_match(text, p, next);
cin.get();
return 0;
}
這裡需要強調一下,KMP演算法的僅當模式與主串之間存在很多部分匹配情況下才能體現它的優勢,部分匹配時KMP的i不需要回溯,否則和樸素模式匹配沒有什麼差別。