KMP 算法的實現過程講解,圖解加算法說明

以前對KMP算法的理解不夠深刻,懂了幾天就忘了。今天心血來潮花了整整一天時間,仔細研究了KMP的算法思想和實現過程。感覺以後再也不會忘了。

KMP是一個效率非常高的字符串匹配算法。不過由於其難以理解,很多程序員一直是半懂的狀態。

kmp算法完成的任務是:給定兩個字符串O和f,長度分別爲n和m,判斷f是否在O中出現,如果出現則返回出現的位置。一般的算法需要兩層循環,算法複雜度是O(n^2).

KMP算法將該任務的算法複雜度降到了O(N)級別。下面介紹一下KMP。


KMP算法思想

我們首先用一個圖來描述kmp算法的思想。在字符串O中尋找f,當匹配到位置i時兩個字符串不相等,這時我們需要將字符串f向前移動。常規方法是每次向前移動一位,但是它沒有考慮前i-1位已經比較過這個事實,所以效率不高。事實上,如果我們提前計算某些信息,就有可能一次前移多位。假設我們根據已經獲得的信息知道可以前移k位,我們分析移位前後的f有什麼特點。我們可以得到如下的結論:


  • A段字符串是f的一個前綴。
  • B段字符串是f的一個後綴。
  • A段字符串和B段字符串相等。

所以前移k位之後,可以繼續比較位置i的前提是f的前i-1個位置滿足:長度爲i-k-1的前綴A和後綴B相同。只有這樣,我們纔可以前移k位後從新的位置繼續比較。


所以kmp算法的核心即是計算字符串f每一個位置之前的字符串的前綴和後綴公共部分的最大長度【也是該位置失配後,模式串應該要重新匹配的位置】(不包括字符串本身,否則最大長度始終是字符串本身)。獲得f每一個位置的最大公共長度之後,就可以利用該最大公共長度快速和字符串O比較。當每次比較到兩個字符串的字符不同時,我們就可以根據最大公共長度將字符串f向前移動(已匹配長度-最大公共長度)位,接着繼續比較下一個位置。事實上,字符串f的前移只是概念上的前移,只要我們在比較的時候從最大公共長度之後比較f和O即可達到字符串f前移的目的。

next數組計算

理解了kmp算法的基本原理,下一步就是要獲得字符串f每一個位置的最大公共長度。這個最大公共長度在算法導論裏面被記爲next數組。在這裏要注意一點,next數組表示的是長度,下標從1開始;但是在遍歷原字符串時,下標還是從0開始。假設我們現在已經求得next[1]、next[2]、……next[i],分別表示長度爲1到i的字符串的前綴和後綴最大公共長度,現在要求next[i+1]。由上圖我們可以看到,如果位置i和位置next[i]處的兩個字符相同(下標從零開始),則next[i+1]等於next[i]加1。如果兩個位置的字符不相同,我們可以將長度爲next[i]的字符串繼續分割,獲得其最大公共長度next[next[i]],然後再和位置i的字符比較。這是因爲長度爲next[i]前綴和後綴都可以分割成上部的構造,如果位置next[next[i]]和位置i的字符相同,則next[i+1]就等於next[next[i]]加1。如果不相等,就可以繼續分割長度爲next[next[i]]的字符串,直到字符串長度爲0爲止。


void getNext(string b, int re[20]){
    int len = b.length();
    int j = 0;
   
    re[0] = re[1] = 0;
    for(int i  = 1; i < len; ++i){
        while((j > 0)&&(b[i]!=b[j])){
            j = re[j];
        }
        if(b[i] == b[j]){
                j++;
        }
        re[i+1] = j;
    }
};//next 數組在re 中返回

next 數組的計算過程如下所示(next 數組空間比原始串多一個,最後一個其實沒有用):

 

 

a

b

c

d

a

b

c   

char

a   

b   

c   

d   

a   

b   

c   

 

index

0

1

2

3

4

5

6

 

next

0

0

0

 

 

 

 

 


     i = 1; next[2] = 0;

 

 

 

a

b

c

d

a

b   

c   

char

a   

b   

c   

d   

a   

b   

c   

 

 

index

0

1

2

3

4

5

6

 

 

next

0

0

0

0

 

 

 

 

 


      i =2; next[3] = 0;

 

 

 

 

a

b

c

d

a   

b   

c   

char

a   

b   

c   

d   

a   

b   

c   

 

 

 

index

0

1

2

3

4

5

6

 

 

 

next

0

0

0

0

0

 

 

 

 

 


      i = 3; next[4] = 0;

 

 

 

 

 

a

b

c

d   

a   

b   

c   

char

a   

b   

c   

d   

a   

b   

c   

 

 

 

 

index

0

1

2

3

4

5

6

 

 

 

 

next

0

0

0

0

0

1

 

 

 

 

 


      i = 4; next[5] = 1;因爲相等,執行 j++,下同

 

 

 

 

 

a

b

c

d   

a   

b   

c   

char

a   

b   

c   

d   

a   

b   

c   

 

 

 

 

index

0

1

2

3

4

5

6

 

 

 

 

next

0

0

0

0

0

1

 

 

 

 

 


     i = 5 next [6] = 2

 

 

 

 

 

a

b

c

d   

a   

b   

c   

char

a   

b   

c   

d   

a   

b   

c   

 

 

 

 

index

0

1

2

3

4

5

6

 

 

 

 

next

0

0

0

0

0

1

2

 

 

 

 

  

    i = 6 next [7] = 3

利用上述算法,可以得到next數組,但是在一種情況下,該next數組工作效率不是很高,考慮模式串是aaaab,此時按照上述算法思想得到的next數組爲 0 0 1 2 3. 但是顯然由於前面的四個字符相等,當在任意一個a的位置失配的時候,不需要一個一個和前面的字符去匹配,而可以直接滑動到第一個a的位置去匹配(因爲中間的都相等,否則就是重複這樣的動作,直到第一個a的位置),因此修改後的函數如下:
void getNext(string b, int re[20]){  
    int len = b.length();  
    int j = 0;  
     
    re[0] = re[1] = 0;  
    //re[0] = 0;
    for(int i  = 1; i < len; ++i){  
        while((j > 0)&&(b[i]!=b[j])){  
            j = re[j];  
        }  
        if(b[i] == b[j]){  
                j++;  
        } 
        if(b[i+1] != b[j]) re[i + 1] = j;
        else re[ i + 1 ] = re[j];
    }  
}



字符串匹配

計算完成next數組之後,我們就可以利用next數組在字符串O中尋找字符串f的出現位置。匹配的代碼和求next數組的代碼非常相似,因爲匹配的過程和求next數組的過程其實是一樣的。假設現在字符串f的前i個位置都和從某個位置開始的字符串O匹配,現在比較第i+1個位置。如果第i+1個位置相同,接着比較第i+2個位置;如果第i+1個位置不同,則出現不匹配,我們依舊要將長度爲i的字符串分割,獲得其最大公共長度next[i],然後從next[i]繼續比較兩個字符串。這個過程和求next數組一致,所以可以匹配代碼如下


int main()
{
	string aim = "abcdabcf";
    string ok = "xdabcdabcff";
    int TEST[20];
    getNext(aim,TEST);
    for (int i = 0; i <= aim.length(); ++i){
        cout<<TEST[i]<<"  ";//output the next array
    }
    cout<<endl;
    // mactching 
    int j = 0;
    for (int i = 0; i < ok.length(); ++i){
        while((j > 0) && (ok[i] != aim[j])){
            j = TEST[j];
        }
        if(ok[i] == aim[j]){
            j++;
        }
        if (j == aim.length()){
            cout<<"found"<<endl;
        }
    }
}
	


 







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章