字符串匹配 -- KMP算法

字符串匹配 -- KMP算法

參考資料
1 數據結構( C 語言版)


概述

    在前面的文章樸素字符串匹配Rabin-Karp算法中,對有關字符串匹配問題做了相關的介紹。其中樸素字符串匹配效率較差,其時間複雜度爲 O ( ( n - m + 1 ) m ) ,當問題規模較大時,該算法的劣勢會變得更加明顯;Rabin-Karp算法需要一個預處理,其時間爲 O ( m ) ,匹配的最壞時間複雜度爲 O ( ( n - m + 1 ) m ) ,但在平均情況和實際情況下,其效果還是不錯的,所以應用還是很廣的。
    事實上還有一種字符串匹配算法 -- KMP算法 ( 有人將此算法戲稱爲看mao片算法,呵呵!!無力吐槽了都!! ),其效率更高。該算法也需要對模式進行預處理,其時間複雜度爲 O ( m ),匹配的時間複雜度爲 O ( n ) 。該算法是由Knuth、Morris、Pratt三人設計的線性時間字符串匹配算法。在此不得不佩服這三位大牛,該算法設計的實在巧妙,代碼簡短精巧,爲了把這個算法看懂還要反覆思考琢磨,花了我不少時間。


算法思想

KMP算法與以往的字符串匹配算法不同的一點是:每當一趟匹配過程中出現比較不匹配時,不需要對字符串進行回溯,而是利用已經得到的“部分匹配”的結果將模式向右滑動儘可能遠的一段距離後,繼續進行比較。
該算法一個最重要的就是對模式進行預處理,也就是通過預處理得到一個數組 next[ ] ,該數組用來說明檔模式中第 j 個字符與主串中相應字符“失配”之後,在模式中需要重新和主串中該字符進行比較的字符的位置。下面將給出求 next 數組的方法。
 
KMP算法的匹配過程
有一個字符串 str = "acabaabaabcac " ,模式 p = "abaabcac" 。模式 p 的 next 數組已得到,如下圖

得到模式 P 的 next 數組之後,就可以對字符串進行匹配了。


匹配過程

我們用兩個指針i和j分別指向 str 和 P 。在匹配之前,我們需要知道 next 大概要完成什麼樣的工作。我們根據模式 P 的特點,
1 字符串 str 與模式 P 進行匹配,str[ 1 ] != P[ 1 ] ,失配。此時我來做些說明。對於一般情況,當失配的時候,str[ i ] ! = P[ j ],說明 str[ i - j  .. i-1] = P[ 0 .. j-1 ] 。
此時,字符串 str 不動,查找模式 P 的 next 數組。next[ 1 ] = 0 ,於是模式從 P[ next [ 1 ] ] 開始與str[ 1 ] 進行比較。


2 此時從 P[ next [ 1 ] ] 也就是 P [ 0 ] 開始與str[ 1 ] 進行比較,失配,此時j = next[ 0] = -1 ,i 和 j 同時都加1,即 i = 2 ,j = 0 ,分別從 str [ 2 ] 和 P [0 ] 開始比較



3  當從 str [ 2 ] 和 P [0 ] 開始比較,一直匹配直到 i = 7 和 j = 5 時失配。此時查找 next[ 5 ] = 2 ,說明 模式 P  在 0~1  與3~ 4 的字符串相等。同時 str[ 3] ~ str[4] = str[6] ~ str[7] 。P [ 0 ] ~ P [ 1 ] 等於 str [ 3 ] ~ str [ 4 ] ,也等於 str [ 6 ] ~ str[ 7 ] ,所以模式在比較的時候 str[ 6 ] ~ str[ 7 ] 就不需要比較了,直接從 j = next[5] = 2 處與 str[ 7 ] 進行比較即可。


4  匹配成功!!




next數組

從上面的匹配過程中,可以看到模式右移是根據 next [ j ] 來決定應該要右移多少。那麼next數組是怎麼得到的呢。
我們看一下求next數組的代碼
void get_next( const char* s , int len , int *next)
{
    int i  = 0;
    next[0] = -1;
    int j = -1;
    while(i < len-1)
    {
        if( j == -1 || s[i] == s[j])  //j=-1:模式開始位置  ;當s[i] = s[j] :模式將繼續往前
        {
            ++i;
            ++j;
            if( s[i] != s[j])  //不相等,則到此結束
                next[i] = j;
            else
                next[i] = next[j];
        }
        else  //當
            j = next[j];
    }
}

根據該代碼,我們可以大致得到以下步驟
1 初始化。 i = 0 , j = -1 next[ 0 ]  = -1
2 執行循環。
3  滿足 j == -1  或 s[ i ] = s[ j ] 時,i ,j 同時加1 ,之後判斷s[ i ] 是否 s[ j ] 。若不等,則next [ i ] = j ;否則 next [ i ] = next [ j ]
4  若不滿足 j == -1  或 s[ i ] = s[ j ] ,則 j 回溯,j = next [ j ] 


 next 求解過程


1 模式p = "abaabcac" 。初始化: i = 0 , j = -1 , next [ 0 ] = -1

2 j == -1 ,++i ,++j 則 i= 1,j = 0。P[1] != P[0] 於是 next [1 ] = 0

3  P[1] != P[0] ,執行else 語句,j = next [ 0] = -1 。此時 i = 1 ,j = -1,進入循環之後,i = 2,j= 0 。P[2 ] = P[ 0 ] ,所以 next [ 2 ] = next [ 0] = -1

4 此時i = 2 , j = 0。進入循環,P[2 ] = P[ 0 ],i = 3,j = 1,P[ 3 ] != P [ 1 ] ,next[3] = 1。
 
5 此時 i = 3 , j = 1。進入循環,P[ 3 ] != P [ 1 ] ,於是執行else 語句,j = next [1 ] = 0。再次比較 P[ 3 ] = P [ 0 ] ,於是 i = 4 , j = 1。P[ 4 ] = P [ 1 ] ,next [ 4  ] = next [ 1 ] = 0
6 此時 i = 4 , j = 1,進入循環,P[ 4 ] = P [ 1 ] ,i= 5,j = 2 ,P[ 5 ] != P [ 2 ] ,next [ 5 ] = 2 

7 此時 i = 5 , j = 2 ,進入循環,P[ 5 ] != P [ 2 ] ,執行else 語句,j = next [2 ]  = -1。於是i = 6 , j = 0。P[ 6 ] = P [ 0 ] ,則next [ 6 ] = next[0] = -1

8 此時 i = 6, j = 0,進入循環,P[ 6 ] = P [ 0 ] ,i = 7 , j = 1 。P[ 7 ] != P [ 1 ] ,於是 next [ 7 ] = j = 1.

到此,整個求解 next 數組完成了。


KMP代碼實現

#include <iostream>
using namespace std;
void get_next( const char* s , int len , int *next)
{
    int i  = 0;
    next[0] = -1;
    int j = -1;
    while(i < len-1)
    {
        if( j == -1 || s[i] == s[j])  //j=-1:模式開始位置  s[i] = s[j] :模式將繼續往前
        {
            ++i;
            ++j;
            if( s[i] != s[j])  
                next[i] = j;
            else
                next[i] = next[j];
        }
        else  //當
            j = next[j];
    }
}

int kmp( const char* s, int slen, const char* p, int plen, const int* next, int pos)
{
    int i = pos;
    int j  = 0;
    while( i < slen && j < plen)
    {
        if( j == -1 || s[i] == p[j])  // s[i] == p[j] : 匹配則繼續向前,s和p同時向前  j=-1表示從模式開頭進行匹配
        {
            ++i;
            ++j;
        }
        else   //不匹配則模式回溯
            j = next[j];
    }
    if( j >= plen)
        return i-plen;
    else
        return -1;
}
int main()
{
        char *str = "acabaabaabcacaabc";
        char *p = "abaabcac";
        int next[8] ={0};
        get_next(p,8,next);
        int pos = kmp(str,17,p,8,next,0);
        cout << "pos:" << pos << endl;
        for(int i = 0 ; i < 8 ; i++)
            cout << next[i] << ' ' ;
            cout << endl;
        return 0;
}




      轉載該文章請註明作者和出處 ,請勿用於任何商業用途
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章