天天看點

KMP字元串比對算法——用最容易了解的方式描述

看了資料結構書上對于快速模式比對算法KMP的介紹,感覺雲裡霧裡。本文根據自己了解,并查資料整理了一種非常清晰簡單的字元串比對算法,并給予實作,自诩原創吧。

字元串比對是我們經常要用到的一種算法,與普通的比對算法相比KMP算法效率更高,時間複雜度為O(m+n)。下面給予詳細講解:

概念詳解

設原字元串為“BBC ABCDAB ABCDABCDABDE”,待比對字元串為“ABCDABD”。

  1. 首先,字元串”BBC ABCDAB ABCDABCDABDE”的第一個字元與搜尋詞”ABCDABD”的第一個字元,進行比較。因為B與A不比對,是以搜尋詞後移一位。
    KMP字元串比對算法——用最容易了解的方式描述
  2. 因為B與A不比對,搜尋詞再往後移。
    KMP字元串比對算法——用最容易了解的方式描述
  3. 就這樣,直到字元串有一個字元,與搜尋詞的第一個字元相同為止。
    KMP字元串比對算法——用最容易了解的方式描述
  4. 接着比較字元串和搜尋詞的下一個字元,還是相同。
    KMP字元串比對算法——用最容易了解的方式描述
  5. 直到字元串有一個字元,與搜尋詞對應的字元不相同為止。
    KMP字元串比對算法——用最容易了解的方式描述
  6. 這時,最自然的反應是,将搜尋詞整個後移一位,再從頭逐個比較。這樣做雖然可行,但是效率很差,因為你要把”搜尋位置”移到已經比較過的位置,重比一遍。
    KMP字元串比對算法——用最容易了解的方式描述
  7. 一個基本事實是,當空格與D不比對時,你其實知道前面六個字元是”ABCDAB”。KMP算法的想法是,設法利用這個已知資訊,不要把”搜尋位置”移回已經比較過的位置,繼續把它向後移,這樣就提高了效率。
    KMP字元串比對算法——用最容易了解的方式描述
  8. 而怎麼記錄這個已知的資訊呢,聰明的人使用了一張”部分比對表”來記錄已有的資訊。如何産生這張表,稍後解釋。
    KMP字元串比對算法——用最容易了解的方式描述
    這就是一張部分比對表。先說一下,部分比對值,是根據字元串字首和字尾算出來的。
  9. 已知空格與D不比對時,前面六個字元”ABCDAB”是比對的。查表可知,最後一個比對字元B對應的”部分比對值”為2,是以按照下面的公式算出向後移動的位數:移動位數 = 已比對的字元數 - 對應的部分比對值
    KMP字元串比對算法——用最容易了解的方式描述
  10. 因為空格與C不比對,搜尋詞還要繼續往後移。這時,已比對的字元數為2(”AB”),對應的”部分比對值”為0。是以,移動位數 = 2 - 0,結果為 2,于是将搜尋詞向後移2位。
    KMP字元串比對算法——用最容易了解的方式描述
  11. 因為空格與A不比對,繼續後移一位。
    KMP字元串比對算法——用最容易了解的方式描述
  12. 逐位比較,直到發現C與D不比對。于是,移動位數 = 6 - 2,繼續将搜尋詞向後移動4位。
    KMP字元串比對算法——用最容易了解的方式描述
  13. 逐位比較,直到搜尋詞的最後一位,發現完全比對,于是搜尋完成。如果還要繼續搜尋(即找出全部比對),移動位數 = 7 - 0,再将搜尋詞向後移動7位,這裡就不再重複了。
    KMP字元串比對算法——用最容易了解的方式描述
  14. 下面介紹《部分比對表》是如何産生的。
    KMP字元串比對算法——用最容易了解的方式描述
      首先,要了解兩個概念:”字首”和”字尾”。 “字首”指除了最後一個字元以外,一個字元串的全部頭部組合;”字尾”指除了第一個字元以外,一個字元串的全部尾部組合。
  15. “部分比對值”就是”字首”和”字尾”的最長的共有元素的長度。以”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。

  16. 部分比對”的實質是,有時候,字元串頭部和尾部會有重複。比如,”ABCDAB”之中有兩個”AB”,那麼它的”部分比對值”就是2(”AB”的長度)。搜尋詞移動的時候,第一個”AB”向後移動4位(字元串長度-部分比對值),就可以來到第二個”AB”的位置。
    KMP字元串比對算法——用最容易了解的方式描述
    如果這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 -;
}
           

繼續閱讀