看了資料結構書上對于快速模式比對算法KMP的介紹,感覺雲裡霧裡。本文根據自己了解,并查資料整理了一種非常清晰簡單的字元串比對算法,并給予實作,自诩原創吧。
字元串比對是我們經常要用到的一種算法,與普通的比對算法相比KMP算法效率更高,時間複雜度為O(m+n)。下面給予詳細講解:
概念詳解
設原字元串為“BBC ABCDAB ABCDABCDABDE”,待比對字元串為“ABCDABD”。
- 首先,字元串”BBC ABCDAB ABCDABCDABDE”的第一個字元與搜尋詞”ABCDABD”的第一個字元,進行比較。因為B與A不比對,是以搜尋詞後移一位。
- 因為B與A不比對,搜尋詞再往後移。
- 就這樣,直到字元串有一個字元,與搜尋詞的第一個字元相同為止。
- 接着比較字元串和搜尋詞的下一個字元,還是相同。
- 直到字元串有一個字元,與搜尋詞對應的字元不相同為止。
- 這時,最自然的反應是,将搜尋詞整個後移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把”搜尋位置”移到已經比較過的位置,重比一遍。
- 一個基本事實是,當空格與D不比對時,你其實知道前面六個字元是”ABCDAB”。KMP算法的想法是,設法利用這個已知資訊,不要把”搜尋位置”移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。
- 而怎麼記錄這個已知的資訊呢,聰明的人使用了一張”部分比對表”來記錄已有的資訊。如何産生這張表,稍後解釋。 這就是一張部分比對表。先說一下,部分比對值,是根據字元串字首和字尾算出來的。
- 已知空格與D不比對時,前面六個字元”ABCDAB”是比對的。查表可知,最後一個比對字元B對應的”部分比對值”為2,是以按照下面的公式算出向後移動的位數:移動位數 = 已比對的字元數 - 對應的部分比對值
- 因為空格與C不比對,搜尋詞還要繼續往後移。這時,已比對的字元數為2(”AB”),對應的”部分比對值”為0。是以,移動位數 = 2 - 0,結果為 2,于是将搜尋詞向後移2位。
- 因為空格與A不比對,繼續後移一位。
- 逐位比較,直到發現C與D不比對。于是,移動位數 = 6 - 2,繼續将搜尋詞向後移動4位。
- 逐位比較,直到搜尋詞的最後一位,發現完全比對,于是搜尋完成。如果還要繼續搜尋(即找出全部比對),移動位數 = 7 - 0,再将搜尋詞向後移動7位,這裡就不再重複了。
- 下面介紹《部分比對表》是如何産生的。 首先,要了解兩個概念:”字首”和”字尾”。 “字首”指除了最後一個字元以外,一個字元串的全部頭部組合;”字尾”指除了第一個字元以外,一個字元串的全部尾部組合。
-
“部分比對值”就是”字首”和”字尾”的最長的共有元素的長度。以”ABCDABD”為例,計算部分比對值:
-“A”的字首和字尾都為空集,共有元素的長度為0;
-“AB”的字首為[A],字尾為[B],共有元素的長度為0;
-“ABC”的字首為[A, AB],字尾為[BC, C],共有元素的長度0;
-“ABCD”的字首為[A, AB, ABC],字尾為[BCD, CD, D],共有元素的長度為0;
-“ABCDA”的字首為[A, AB, ABC, ABCD],字尾為[BCDA, CDA, DA, A],共有元素為”A”,長度為1;
-“ABCDAB”的字首為[A, AB, ABC, ABCD, ABCDA],字尾為[BCDAB, CDAB, DAB, AB, B],共有元素為”AB”,長度為2;
-“ABCDABD”的字首為[A, AB, ABC, ABCD, ABCDA, ABCDAB],字尾為[BCDABD, CDABD, DABD, ABD, BD, D],共有元素的長度為0。
- 部分比對”的實質是,有時候,字元串頭部和尾部會有重複。比如,”ABCDAB”之中有兩個”AB”,那麼它的”部分比對值”就是2(”AB”的長度)。搜尋詞移動的時候,第一個”AB”向後移動4位(字元串長度-部分比對值),就可以來到第二個”AB”的位置。 如果這6個已比對的字元中沒有部分比對值,說明這幾個字元都不能再和現有字元成功比對了,是以指針直接移動到這幾個字元的後面。
執行個體代碼
首先計算出部分比對表,再進行比對
void get_match_table(const char* p, int* next)
{
int i, n, k;
n = strlen(p);
next[] = next[] = ;
k = ; /* 第i次疊代開始之前,k表示next[i-1]的值 */
for (i = ; i <= n; i++) {
for (; k != && p[k] != p[i-]; k = next[k]);
if (p[k] == p[i-]) 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 = ; /* q表示上一次疊代比對了多少個字元,
s表示這次疊代從text的哪個字元開始比較 */
while (s < n) {
for (q = next[q]; q < m && p[q] == text[s]; q++, s++);
if (q == ) s++;
else if (q == m) {
printf("比對成功:%d\n", s-m);
}
}
}
測試代碼:
int next[], n;
char *p = "ABCDABD";
char *text = "BBC ABCDAB ABCDABCDABDE";
get_match_table(p,next);
kmp_match(text, p, next);
kmp難以了解,是公認的。下一篇部落格我将介紹一個更高效率,且更常用更容易了解的字元串比對算法——字元串比對的Boyer-Moore算法。
這裡又發現了一個比上面更快一點的算法:
//更高效的方法,首先與第一字元比,直到比對。。。
void get_match_table2(const char* p, int* next)
{
int i,j,n;
n = strlen(p);
i = ;j = -;
while(i<n){
if (j == - || p[i] == p[j]){
i++;j++;next[i] = j;
}else{
j = next[j];
}
}
}
int kmp_match2(char *text, char *p, int *next)
{
int i,j;
int nt,np;
i =;j=;
nt = strlen(text);
np = strlen(p);
while(i<nt && j<np){
if (j == - || text[i] == p[j]){
i++;j++;
}else{
j = next[j];
}
}
if (j == np)
return i - np;
else
return -;
}