1. 程式人生 > >KMP演算法解析與實現

KMP演算法解析與實現

最近學了KMP演算法,學了半天終於學會了,感覺還是挺難理解的,在這裡與大家分享一下我的思考,也方便我自己複習。
下面就正式進入講解環節吧(本文所有字串初始座標為0!)。

首先我們要明確KMP演算法是幹什麼的:我們有兩個字串T與S,現在我們要找到T中為S的子串,求出滿足條件子串數量及位置。這時候我們就要用到KMP演算法。

傳統演算法回顧:

在講解KMP演算法前,我們先看一下傳統方式的查詢。
在字串T與S中各設一個指標i,j=1;依次比對i+1,j+1、i+2,j+2……..
如果發現有不同的,則比對i+1,j=1;
用圖來看一下(加粗為當前比對位置):
T: c1 c2 c3 c4 c5
S: c1

c2 d3 e4
然後
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
然後
T: c1 c2 c3 c4 c5
S: c1 c2 d3 e4
這時我們發現c3與d3不同,就比對T-c2與S-c1,即:
T: c1 c2 c3 c4 c5
S: __ c1 c2 d3 e4
這樣的比對,掃描T需要O(n)時間,檢查比對需要O(n)時間。
總時間複雜度:O( n^2 ),這樣的複雜度還算快的。
然而與KMP相比,還是太慢了,KMP可以把時間複雜度優化到O(n+m);
下面讓我們來具體看看KMP的實現。

運用next陣列實現跳轉:

先引入兩個概念:
- 字首串:例如abcde,則其字首依次為:a,ab,abc,abcd,abcde
- 字尾串:例如abcde,則其後綴串依次為e,de,cde,bcde,abcde
對於ababa這個目標串(對比串),我們可以發現:
其字首串與字尾串有相同部分!這就是KMP實現的關鍵。
比對abacdabe與ababa時,我們可以看到:
T: a b a c

d a b e
S: a b a a b a
有第三位置<初始座標為0>上的a與c不同。
當我們將S往後移動時由於第一位與第三位相同,前兩位已經比對過了。
所以後移後絕對不會匹配.
這樣就浪費了大量的時間。
而KMP的比對就會直接跳到:
T: a b a c d a b e
S:——a b a a b a
具體是如何實現的呢?我們引入一個F[i]來記錄目標串S的字首串與字尾串的最大相同量。
例如:a b a a b a
則對於未知i,有:
i=1,a,F[1]=無;
i=2,a b,F[2]=無;
i=3,a b a,F[3]=1;
i=4,a b a a,F[4]=1;
i=5,a b a a b,F[5]=2;
i=6,a b a a b a,F[6]=3;
但為了方便計數(等會你就知道為什麼了),也方便區分無相同字首的位置,我們將所有F減1,即無相同字首的為-1;
所以:
F[1]=-1;F[2]=-1;F[3]=0;F[4]=0;F[5]=-1;F[6]=2;
下文中所有的表示都是這樣的。
然後我們用幾個例子看一看next是如何實現跳轉的。
例如匹配abaaddddd與abaaba,則用字首陣列的方法為:
字首陣列:a -b a a -b a(‘-‘方便對齊)
———— -1 -1 0 0 1 2
匹配到:
T: a b a a d
d d d d
S: a b a a b a
F[5-1]=0,有一個相同的字首,j直接跳轉到F[j-1]+1=1(注意初始下標為0);即:
T: a b a a d d d d d
S: ——–a b a a b a
此時匹配位置前一個的’a’已經匹配,實現跳轉。再看一個:
匹配abaabdddd與abaaba,匹配到:
T: a b a a b d d d d
S: a b a a b a
此時F[6-1]=1,有1+1=2個相同字首,j跳轉到F[j-1]+1=2,即:
T: a b a a b d d d d
S: ——–a b a a b a
讀者們可以仔細體會一下上面兩個例子,這裡我給出跳轉的總結:

if(t[i]!=s[j])
{
   if(j==0)i++;    //沒有相同字首,直接向後移(與一般方法相同)
   else j=F[j-1]+1;    //跳轉,KMP
}

求解next陣列:

知道如何用next陣列實現跳轉後,我們還要想辦法實現next陣列的求解。
對於next陣列,我們求解的方法與上文其實差不多,但是用到了一個思想:自己匹配自己!
我們先思考:求解next陣列肯定要用O(m)的演算法吧。(要保證KMP的時間複雜度)。
這裡的思路是一個遞推思路。
結合下面的程式碼讀者們不妨先想一想,什麼樣的情況下F[i]可以由前面的F[k]加1求得。
先給出求解F陣列的程式碼:

void get_low()        
{
    len=t.length();
    F[0]=-1;    
    int temp=0,i;
    for(i=1;i<=len-1;i++)      //初始下標為0!
    {
        int j=F[i-1];          
        while(t[j+1]!=t[i]&&j>=0)j=F[j];                
        if(t[j+1]==t[i])F[i]=j+1;  
        else F[i]=-1;
    }return;
}

還是先從例子開始吧。
所求字首陣列:
S: a _b a a b _b a b a a b
F: -1 -1 0 0 1 -1 0 1 2 3 4

我們求解第二個:
S:a __b a a b
F:-1 -1
此時這個a的前一個b的F值為-1,所以此時的a不能由b的F[1]求出,j=-1;
比對S[j+1]=a,與當前位a相同,所以F[i]=F[2]=j+1=0;

我們求解第五個:
S= a b a a b b
F=-1 -1 0 0
此時j=F[i-1]=F[4]=0,但當前位置b與前一個a不同,無法接上,j=F[j]=0;
然後比對S[j+1]=S[2]=’b’與當前位b相同,F[i]=F[5]=j+1=1;

這一部分雖然程式碼很短,但這是KMP的難點,比較難理解(但一定不能背代 碼),希望讀者們進行一次完整的模擬以助於理解(對照上文的程式碼),這裡給出一個典型的例子以供參考:
–典型例子:S: a _b a a b _b a b a a b //下標線為方便對齊
–對應next:F:-1 -1 0 0 1 -1 0 1 2 3 4
這個例子把next的所有情況都包含到了,希望讀者們一定要耐著性子模擬一遍。

理解了以上內容後(我承認有點難度),KMP演算法的實現應該也就不難了,下面我給出完整的程式碼<內含用next匹配的具體程式碼>:

模板題來自於:洛谷 P3375 【模板】KMP字串匹配

題目描述

如題,給出兩個字串s1和s2,其中s2為s1的子串,求出s2在s1中所有出現的位置。

為了減少騙分的情況,接下來還要輸出子串的字首陣列next。

(如果你不知道這是什麼意思也不要問,去百度搜[kmp演算法]學習一下就知道了。)
輸入輸出格式
輸入格式:

第一行為一個字串,即為s1(僅包含大寫字母)

第二行為一個字串,即為s2(僅包含大寫字母)

輸出格式:

若干行,每行包含一個整數,表示s2在s1中出現的位置

接下來1行,包括length(s2)個整數,表示字首陣列next[i]的值。

輸入輸出樣例
輸入樣例#1:

ABABABC
ABA

輸出樣例#1:

1
3
0 0 1

說明

時空限制:1000ms,128M
設s1長度為N,s2長度為M
對於100%的資料:N<=1000000,M<=1000

完整程式碼(即KMP演算法的模板):

#include<cstdio>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

char t[10000002],s[10002];  //原串、模式串
int F[10002];     //字首next陣列
int len,len1;

inline void get_bef()   //求解next字首陣列
{
    int i,j;
    F[0]=-1;len=strlen(s);
    for(i=1;i<=len-1;i++)
    {
        j=F[i-1];
        while(j>=0 && s[i]!=s[j+1])j=F[j];
        if(s[i]!=s[j+1])F[i]=-1;
        else F[i]=j+1;
    }return;
}

inline void find_pos()  //求解位置
{
    int i,j;
    len1=strlen(t);j=0;i=0;
    while(i<=len1-1)
    {
        if(t[i]==s[j])              //這裡為正常匹配
        {
            i++;
            j++;
            if(j==len)printf("%d\n",i-j+1);
        }
        else                        //KMP跳轉
        {
            if(j==0)i++;
            else j=F[j-1]+1;           
        }
    }return; 
}

int main()
{
    scanf("%s",&t);
    scanf("%s",&s);
    get_bef();
    find_pos();
    for(int i=0;i<=len-1;i++)printf("%d ",F[i]+1);
    return 0;
 }

希望我的一點理解感悟能夠幫助到oier們,呵呵呵,謝謝觀看。